File: System\Text\Json\Schema\JsonSchema.cs
Web Access
Project: src\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 MinLengthPropertyName = "minLength";
        internal const string MaxLengthPropertyName = "maxLength";
 
        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 => _ref; set { VerifyMutable(); _ref = value; } }
        private string? _ref;
 
        public string? Comment { get => _comment; set { VerifyMutable(); _comment = value; } }
        private string? _comment;
 
        public JsonSchemaType Type { get => _type; set { VerifyMutable(); _type = value; } }
        private JsonSchemaType _type = JsonSchemaType.Any;
 
        public string? Format { get => _format; set { VerifyMutable(); _format = value; } }
        private string? _format;
 
        public string? Pattern { get => _pattern; set { VerifyMutable(); _pattern = value; } }
        private string? _pattern;
 
        public JsonNode? Constant { get => _constant; set { VerifyMutable(); _constant = value; } }
        private JsonNode? _constant;
 
        public List<KeyValuePair<string, JsonSchema>>? Properties { get => _properties; set { VerifyMutable(); _properties = value; } }
        private List<KeyValuePair<string, JsonSchema>>? _properties;
 
        public List<string>? Required { get => _required; set { VerifyMutable(); _required = value; } }
        private List<string>? _required;
 
        public JsonSchema? Items { get => _items; set { VerifyMutable(); _items = value; } }
        private JsonSchema? _items;
 
        public JsonSchema? AdditionalProperties { get => _additionalProperties; set { VerifyMutable(); _additionalProperties = value; } }
        private JsonSchema? _additionalProperties;
 
        public JsonArray? Enum { get => _enum; set { VerifyMutable(); _enum = value; } }
        private JsonArray? _enum;
 
        public JsonSchema? Not { get => _not; set { VerifyMutable(); _not = value; } }
        private JsonSchema? _not;
 
        public List<JsonSchema>? AnyOf { get => _anyOf; set { VerifyMutable(); _anyOf = value; } }
        private List<JsonSchema>? _anyOf;
 
        public bool HasDefaultValue { get => _hasDefaultValue; set { VerifyMutable(); _hasDefaultValue = value; } }
        private bool _hasDefaultValue;
 
        public JsonNode? DefaultValue { get => _defaultValue; set { VerifyMutable(); _defaultValue = value; } }
        private JsonNode? _defaultValue;
 
        public int? MinLength { get => _minLength; set { VerifyMutable(); _minLength = value; } }
        private int? _minLength;
 
        public int? MaxLength { get => _maxLength; set { VerifyMutable(); _maxLength = value; } }
        private int? _maxLength;
 
        public JsonSchemaExporterContext? ExporterContext { get; set; }
 
        public int KeywordCount
        {
            get
            {
                if (_trueOrFalse != null)
                {
                    // Boolean schemas admit no keywords
                    return 0;
                }
 
                int count = 0;
                Count(Ref != null);
                Count(Comment != null);
                Count(Type != JsonSchemaType.Any);
                Count(Format != null);
                Count(Pattern != null);
                Count(Constant != null);
                Count(Properties != null);
                Count(Required != null);
                Count(Items != null);
                Count(AdditionalProperties != null);
                Count(Enum != null);
                Count(Not != null);
                Count(AnyOf != null);
                Count(HasDefaultValue);
                Count(MinLength != null);
                Count(MaxLength != null);
 
                return count;
 
                void Count(bool isKeywordSpecified)
                {
                    count += isKeywordSpecified ? 1 : 0;
                }
            }
        }
 
        public void MakeNullable()
        {
            if (_trueOrFalse != 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 != null)
            {
                objSchema.Add(RefPropertyName, Ref);
            }
 
            if (Comment != null)
            {
                objSchema.Add(CommentPropertyName, Comment);
            }
 
            if (MapSchemaType(Type) is JsonNode type)
            {
                objSchema.Add(TypePropertyName, type);
            }
 
            if (Format != null)
            {
                objSchema.Add(FormatPropertyName, Format);
            }
 
            if (Pattern != null)
            {
                objSchema.Add(PatternPropertyName, Pattern);
            }
 
            if (Constant != null)
            {
                objSchema.Add(ConstPropertyName, Constant);
            }
 
            if (Properties != 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 != null)
            {
                var requiredArray = new JsonArray();
                foreach (string requiredProperty in Required)
                {
                    requiredArray.Add((JsonNode)requiredProperty);
                }
 
                objSchema.Add(RequiredPropertyName, requiredArray);
            }
 
            if (Items != null)
            {
                objSchema.Add(ItemsPropertyName, Items.ToJsonNode(options));
            }
 
            if (AdditionalProperties != null)
            {
                objSchema.Add(AdditionalPropertiesPropertyName, AdditionalProperties.ToJsonNode(options));
            }
 
            if (Enum != null)
            {
                objSchema.Add(EnumPropertyName, Enum);
            }
 
            if (Not != null)
            {
                objSchema.Add(NotPropertyName, Not.ToJsonNode(options));
            }
 
            if (AnyOf != 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);
            }
 
            return CompleteSchema(objSchema);
 
            JsonNode CompleteSchema(JsonNode schema)
            {
                if (ExporterContext is { } context)
                {
                    Debug.Assert(options.TransformSchemaNode != 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,
                };
            }
        }
    }
}