File: src\Shared\JsonSchemaExporter\JsonSchemaExporter.JsonSchema.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.AI.Abstractions\Microsoft.Extensions.AI.Abstractions.csproj (Microsoft.Extensions.AI.Abstractions)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#if !NET9_0_OR_GREATER
using System.Collections.Generic;
using System.Diagnostics;
using System.Text.Json.Nodes;
 
namespace System.Text.Json.Schema;
 
#pragma warning disable SA1204 // Static elements should appear before instance elements
#pragma warning disable S1144 // Unused private types or members should be removed
 
internal static partial class JsonSchemaExporter
{
    // Simple JSON schema representation taken from System.Text.Json
    // https://github.com/dotnet/runtime/blob/50d6cad649aad2bfa4069268eddd16fd51ec5cf3/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchema.cs
    private sealed class JsonSchema
    {
        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;
        private readonly bool? _trueOrFalse;
 
        public string? Schema
        {
            get => _schema;
            set
            {
                VerifyMutable();
                _schema = value;
            }
        }
 
        private string? _schema;
 
        public string? Title
        {
            get => _title;
            set
            {
                VerifyMutable();
                _title = value;
            }
        }
 
        private string? _title;
 
        public string? Description
        {
            get => _description;
            set
            {
                VerifyMutable();
                _description = value;
            }
        }
 
        private string? _description;
 
        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? GenerationContext { get; set; }
 
        public int KeywordCount
        {
            get
            {
                if (_trueOrFalse != null)
                {
                    return 0;
                }
 
                int count = 0;
                Count(Schema != null);
                Count(Ref != null);
                Count(Comment != null);
                Count(Title != null);
                Count(Description != 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)
            {
                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 (Schema != null)
            {
                objSchema.Add(JsonSchemaConstants.SchemaPropertyName, Schema);
            }
 
            if (Title != null)
            {
                objSchema.Add(JsonSchemaConstants.TitlePropertyName, Title);
            }
 
            if (Description != null)
            {
                objSchema.Add(JsonSchemaConstants.DescriptionPropertyName, Description);
            }
 
            if (Ref != null)
            {
                objSchema.Add(JsonSchemaConstants.RefPropertyName, Ref);
            }
 
            if (Comment != null)
            {
                objSchema.Add(JsonSchemaConstants.CommentPropertyName, Comment);
            }
 
            if (MapSchemaType(Type) is JsonNode type)
            {
                objSchema.Add(JsonSchemaConstants.TypePropertyName, type);
            }
 
            if (Format != null)
            {
                objSchema.Add(JsonSchemaConstants.FormatPropertyName, Format);
            }
 
            if (Pattern != null)
            {
                objSchema.Add(JsonSchemaConstants.PatternPropertyName, Pattern);
            }
 
            if (Constant != null)
            {
                objSchema.Add(JsonSchemaConstants.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(JsonSchemaConstants.PropertiesPropertyName, properties);
            }
 
            if (Required != null)
            {
                var requiredArray = new JsonArray();
                foreach (string requiredProperty in Required)
                {
                    requiredArray.Add((JsonNode)requiredProperty);
                }
 
                objSchema.Add(JsonSchemaConstants.RequiredPropertyName, requiredArray);
            }
 
            if (Items != null)
            {
                objSchema.Add(JsonSchemaConstants.ItemsPropertyName, Items.ToJsonNode(options));
            }
 
            if (AdditionalProperties != null)
            {
                objSchema.Add(JsonSchemaConstants.AdditionalPropertiesPropertyName, AdditionalProperties.ToJsonNode(options));
            }
 
            if (Enum != null)
            {
                objSchema.Add(JsonSchemaConstants.EnumPropertyName, Enum);
            }
 
            if (Not != null)
            {
                objSchema.Add(JsonSchemaConstants.NotPropertyName, Not.ToJsonNode(options));
            }
 
            if (AnyOf != null)
            {
                JsonArray anyOfArray = new();
                foreach (JsonSchema schema in AnyOf)
                {
                    anyOfArray.Add(schema.ToJsonNode(options));
                }
 
                objSchema.Add(JsonSchemaConstants.AnyOfPropertyName, anyOfArray);
            }
 
            if (HasDefaultValue)
            {
                objSchema.Add(JsonSchemaConstants.DefaultPropertyName, DefaultValue);
            }
 
            if (MinLength is int minLength)
            {
                objSchema.Add(JsonSchemaConstants.MinLengthPropertyName, (JsonNode)minLength);
            }
 
            if (MaxLength is int maxLength)
            {
                objSchema.Add(JsonSchemaConstants.MaxLengthPropertyName, (JsonNode)maxLength);
            }
 
            return CompleteSchema(objSchema);
 
            JsonNode CompleteSchema(JsonNode schema)
            {
                if (GenerationContext 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;
            }
        }
 
        public static void EnsureMutable(ref JsonSchema schema)
        {
            switch (schema._trueOrFalse)
            {
                case false:
                    schema = new JsonSchema { Not = JsonSchema.CreateTrueSchema() };
                    break;
                case true:
                    schema = new JsonSchema();
                    break;
            }
        }
 
        private static readonly JsonSchemaType[] _schemaValues = new JsonSchemaType[]
        {
            // 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");
        }
 
        private 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 _schemaValues)
            {
                if ((schemaType & type) != 0)
                {
                    array.Add((JsonNode)ToIdentifier(type)!);
                }
            }
 
            return array;
 
            static string? ToIdentifier(JsonSchemaType schemaType) => schemaType switch
            {
                JsonSchemaType.Null => "null",
                JsonSchemaType.Boolean => "boolean",
                JsonSchemaType.Integer => "integer",
                JsonSchemaType.Number => "number",
                JsonSchemaType.String => "string",
                JsonSchemaType.Array => "array",
                JsonSchemaType.Object => "object",
                _ => null,
            };
        }
    }
 
    [Flags]
    private enum JsonSchemaType
    {
        Any = 0, // No type declared on the schema
        Null = 1,
        Boolean = 2,
        Integer = 4,
        Number = 8,
        String = 16,
        Array = 32,
        Object = 64,
    }
}
#endif