| File: System\Text\Json\Schema\JsonSchema.cs | Web Access |
| Project: src\runtime\src\libraries\System.Text.Json\src\System.Text.Json.csproj (System.Text.Json) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Diagnostics; using System.Text.Json.Nodes; namespace System.Text.Json.Schema { internal sealed class JsonSchema { internal const string RefPropertyName = "$ref"; internal const string CommentPropertyName = "$comment"; internal const string TypePropertyName = "type"; internal const string FormatPropertyName = "format"; internal const string PatternPropertyName = "pattern"; internal const string PropertiesPropertyName = "properties"; internal const string RequiredPropertyName = "required"; internal const string ItemsPropertyName = "items"; internal const string AdditionalPropertiesPropertyName = "additionalProperties"; internal const string EnumPropertyName = "enum"; internal const string NotPropertyName = "not"; internal const string AnyOfPropertyName = "anyOf"; internal const string ConstPropertyName = "const"; internal const string DefaultPropertyName = "default"; internal const string ContentEncodingPropertyName = "contentEncoding"; internal const string MinLengthPropertyName = "minLength"; internal const string MaxLengthPropertyName = "maxLength"; internal const string DeprecatedPropertyName = "deprecated"; public static JsonSchema CreateFalseSchema() => new(false); public static JsonSchema CreateTrueSchema() => new(true); public JsonSchema() { } private JsonSchema(bool trueOrFalse) { _trueOrFalse = trueOrFalse; } public bool IsTrue => _trueOrFalse is true; public bool IsFalse => _trueOrFalse is false; /// <summary> /// Per the JSON schema core specification section 4.3 /// (https://json-schema.org/draft/2020-12/json-schema-core#name-json-schema-documents) /// A JSON schema must either be an object or a boolean. /// We represent false and true schemas using this flag. /// It is not possible to specify keywords in boolean schemas. /// </summary> private readonly bool? _trueOrFalse; public string? Ref { get; set { VerifyMutable(); field = value; } } public string? Comment { get; set { VerifyMutable(); field = value; } } public JsonSchemaType Type { get; set { VerifyMutable(); field = value; } } = JsonSchemaType.Any; public string? Format { get; set { VerifyMutable(); field = value; } } public string? Pattern { get; set { VerifyMutable(); field = value; } } public JsonNode? Constant { get; set { VerifyMutable(); field = value; } } public List<KeyValuePair<string, JsonSchema>>? Properties { get; set { VerifyMutable(); field = value; } } public List<string>? Required { get; set { VerifyMutable(); field = value; } } public JsonSchema? Items { get; set { VerifyMutable(); field = value; } } public JsonSchema? AdditionalProperties { get; set { VerifyMutable(); field = value; } } public JsonArray? Enum { get; set { VerifyMutable(); field = value; } } public JsonSchema? Not { get; set { VerifyMutable(); field = value; } } public List<JsonSchema>? AnyOf { get; set { VerifyMutable(); field = value; } } public bool HasDefaultValue { get; set { VerifyMutable(); field = value; } } public JsonNode? DefaultValue { get; set { VerifyMutable(); field = value; } } public int? MinLength { get; set { VerifyMutable(); field = value; } } public int? MaxLength { get; set { VerifyMutable(); field = value; } } public bool? Deprecated { get; set { VerifyMutable(); field = value; } } public string? ContentEncoding { get; set { VerifyMutable(); field = value; } } public JsonSchemaExporterContext? ExporterContext { get; set; } public int KeywordCount { get { if (_trueOrFalse is not null) { // Boolean schemas admit no keywords return 0; } int count = 0; Count(Ref is not null); Count(Comment is not null); Count(Type != JsonSchemaType.Any); Count(Format is not null); Count(Pattern is not null); Count(Constant is not null); Count(Properties is not null); Count(Required is not null); Count(Items is not null); Count(AdditionalProperties is not null); Count(Enum is not null); Count(Not is not null); Count(AnyOf is not null); Count(HasDefaultValue); Count(MinLength is not null); Count(MaxLength is not null); Count(Deprecated is not null); Count(ContentEncoding is not null); return count; void Count(bool isKeywordSpecified) { count += isKeywordSpecified ? 1 : 0; } } } public void MakeNullable() { if (_trueOrFalse is not null) { // boolean schemas do not admit type keywords. return; } if (Type != JsonSchemaType.Any) { Type |= JsonSchemaType.Null; } } public JsonNode ToJsonNode(JsonSchemaExporterOptions options) { if (_trueOrFalse is { } boolSchema) { return CompleteSchema((JsonNode)boolSchema); } var objSchema = new JsonObject(); if (Ref is not null) { objSchema.Add(RefPropertyName, Ref); } if (Comment is not null) { objSchema.Add(CommentPropertyName, Comment); } if (MapSchemaType(Type) is JsonNode type) { objSchema.Add(TypePropertyName, type); } if (Format is not null) { objSchema.Add(FormatPropertyName, Format); } if (Pattern is not null) { objSchema.Add(PatternPropertyName, Pattern); } if (Constant is not null) { objSchema.Add(ConstPropertyName, Constant); } if (Properties is not null) { var properties = new JsonObject(); foreach (KeyValuePair<string, JsonSchema> property in Properties) { properties.Add(property.Key, property.Value.ToJsonNode(options)); } objSchema.Add(PropertiesPropertyName, properties); } if (Required is not null) { var requiredArray = new JsonArray(); foreach (string requiredProperty in Required) { requiredArray.Add((JsonNode)requiredProperty); } objSchema.Add(RequiredPropertyName, requiredArray); } if (Items is not null) { objSchema.Add(ItemsPropertyName, Items.ToJsonNode(options)); } if (AdditionalProperties is not null) { objSchema.Add(AdditionalPropertiesPropertyName, AdditionalProperties.ToJsonNode(options)); } if (Enum is not null) { objSchema.Add(EnumPropertyName, Enum); } if (Not is not null) { objSchema.Add(NotPropertyName, Not.ToJsonNode(options)); } if (AnyOf is not null) { JsonArray anyOfArray = []; foreach (JsonSchema schema in AnyOf) { anyOfArray.Add(schema.ToJsonNode(options)); } objSchema.Add(AnyOfPropertyName, anyOfArray); } if (HasDefaultValue) { objSchema.Add(DefaultPropertyName, DefaultValue); } if (MinLength is int minLength) { objSchema.Add(MinLengthPropertyName, (JsonNode)minLength); } if (MaxLength is int maxLength) { objSchema.Add(MaxLengthPropertyName, (JsonNode)maxLength); } if (Deprecated is { } deprecated) { objSchema.Add(DeprecatedPropertyName, (JsonNode)deprecated); } if (ContentEncoding is not null) { objSchema.Add(ContentEncodingPropertyName, ContentEncoding); } return CompleteSchema(objSchema); JsonNode CompleteSchema(JsonNode schema) { if (ExporterContext is { } context) { Debug.Assert(options.TransformSchemaNode is not null, "context should only be populated if a callback is present."); // Apply any user-defined transformations to the schema. return options.TransformSchemaNode(context, schema); } return schema; } } /// <summary> /// If the schema is boolean, replaces it with a semantically /// equivalent object schema that allows appending keywords. /// </summary> public static void EnsureMutable(ref JsonSchema schema) { switch (schema._trueOrFalse) { case false: schema = new JsonSchema { Not = CreateTrueSchema() }; break; case true: schema = new JsonSchema(); break; } } private static ReadOnlySpan<JsonSchemaType> s_schemaValues => [ // NB the order of these values influences order of types in the rendered schema JsonSchemaType.String, JsonSchemaType.Integer, JsonSchemaType.Number, JsonSchemaType.Boolean, JsonSchemaType.Array, JsonSchemaType.Object, JsonSchemaType.Null, ]; private void VerifyMutable() { Debug.Assert(_trueOrFalse is null, "Schema is not mutable"); if (_trueOrFalse is not null) { Throw(); static void Throw() => throw new InvalidOperationException(); } } public static JsonNode? MapSchemaType(JsonSchemaType schemaType) { if (schemaType is JsonSchemaType.Any) { return null; } if (ToIdentifier(schemaType) is string identifier) { return identifier; } var array = new JsonArray(); foreach (JsonSchemaType type in s_schemaValues) { if ((schemaType & type) != 0) { array.Add((JsonNode)ToIdentifier(type)!); } } return array; static string? ToIdentifier(JsonSchemaType schemaType) { return schemaType switch { JsonSchemaType.Null => "null", JsonSchemaType.Boolean => "boolean", JsonSchemaType.Integer => "integer", JsonSchemaType.Number => "number", JsonSchemaType.String => "string", JsonSchemaType.Array => "array", JsonSchemaType.Object => "object", _ => null, }; } } } }