File: Model\GenAI\GenAISchemaHelpers.cs
Web Access
Project: src\src\Aspire.Dashboard\Aspire.Dashboard.csproj (Aspire.Dashboard)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text.Json.Nodes;
using Microsoft.OpenApi;
 
namespace Aspire.Dashboard.Model.GenAI;
 
internal static class GenAISchemaHelpers
{
    private const string TypeNull = "null";
    private const string TypeBoolean = "boolean";
    private const string TypeInteger = "integer";
    private const string TypeNumber = "number";
    private const string TypeString = "string";
    private const string TypeObject = "object";
    private const string TypeArray = "array";
 
    internal static OpenApiSchema? ParseOpenApiSchema(JsonObject schemaObj)
    {
        var schema = new OpenApiSchema
        {
            Type = ParseTypeValue(schemaObj["type"]),
            Description = schemaObj["description"]?.GetValue<string>()
        };
 
        // Parse properties
        if (schemaObj["properties"] is JsonObject propsObj)
        {
            schema.Properties = new Dictionary<string, IOpenApiSchema>();
            foreach (var prop in propsObj)
            {
                if (prop.Value is JsonObject propSchemaObj)
                {
                    var parsedSchema = ParseOpenApiSchema(propSchemaObj);
                    if (parsedSchema != null)
                    {
                        schema.Properties[prop.Key] = parsedSchema;
                    }
                }
            }
        }
 
        // Parse items (for array types)
        if (schemaObj["items"] is JsonObject itemsObj)
        {
            schema.Items = ParseOpenApiSchema(itemsObj);
        }
 
        // Parse required
        if (schemaObj["required"] is JsonArray requiredArray)
        {
            schema.Required = new HashSet<string>();
            foreach (var item in requiredArray)
            {
                if (item != null)
                {
                    schema.Required.Add(item.GetValue<string>());
                }
            }
        }
 
        // Parse enum
        if (schemaObj["enum"] is JsonArray enumArray)
        {
            schema.Enum = new List<JsonNode>();
            foreach (var item in enumArray)
            {
                if (item != null)
                {
                    schema.Enum.Add(item);
                }
            }
        }
 
        return schema;
    }
 
    internal static JsonSchemaType? ParseTypeValue(JsonNode? typeNode)
    {
        if (typeNode == null)
        {
            return null;
        }
 
        // Handle both string and array of strings for the "type" field
        // In JSON Schema, "type" can be either:
        // - a string: "string"
        // - an array: ["string", "null"]
        if (typeNode is JsonArray typeArray)
        {
            // When type is an array, OR all the types together since JsonSchemaType is a flags enum
            JsonSchemaType? result = null;
            foreach (var item in typeArray)
            {
                if (item != null)
                {
                    var typeValue = item.GetValue<string>();
                    if (TryConvertToJsonSchemaType(typeValue, out var schemaType))
                    {
                        result = result.HasValue ? result.Value | schemaType : schemaType;
                    }
                }
            }
            return result;
        }
 
        // Handle string type
        var typeString = typeNode.GetValue<string>();
        return TryConvertToJsonSchemaType(typeString, out var type) ? type : null;
    }
 
    internal static bool TryConvertToJsonSchemaType(string? typeString, out JsonSchemaType schemaType)
    {
        schemaType = typeString switch
        {
            TypeNull => JsonSchemaType.Null,
            TypeBoolean => JsonSchemaType.Boolean,
            TypeInteger => JsonSchemaType.Integer,
            TypeNumber => JsonSchemaType.Number,
            TypeString => JsonSchemaType.String,
            TypeObject => JsonSchemaType.Object,
            TypeArray => JsonSchemaType.Array,
            _ => default
        };
 
        return schemaType != default;
    }
 
    internal static IList<string> ConvertTypeToNames(IOpenApiSchema? schema)
    {
        if (schema?.Type == null)
        {
            return Array.Empty<string>();
        }
 
        var names = new List<string>();
        var type = ResolveNullInSchemaType(schema.Type.Value);
 
        if (type.HasFlag(JsonSchemaType.Null))
        {
            names.Add(TypeNull);
        }
        if (type.HasFlag(JsonSchemaType.Boolean))
        {
            names.Add(TypeBoolean);
        }
        if (type.HasFlag(JsonSchemaType.Integer))
        {
            names.Add(TypeInteger);
        }
        if (type.HasFlag(JsonSchemaType.Number))
        {
            names.Add(TypeNumber);
        }
        if (type.HasFlag(JsonSchemaType.String))
        {
            names.Add(TypeString);
        }
        if (type.HasFlag(JsonSchemaType.Object))
        {
            names.Add(TypeObject);
        }
        if (type.HasFlag(JsonSchemaType.Array))
        {
            names.Add(GetArrayTypeName(schema.Items));
        }
 
        return names;
 
        static JsonSchemaType ResolveNullInSchemaType(JsonSchemaType type)
        {
            // Only remove null if type isn't just null
            return type == JsonSchemaType.Null ? type : type & ~JsonSchemaType.Null;
        }
 
        static string GetArrayTypeName(IOpenApiSchema? itemsSchema)
        {
            if (itemsSchema?.Type != null)
            {
                // Don't nest arrays.
                var itemType = ResolveNullInSchemaType(itemsSchema.Type.Value);
                if (itemType != JsonSchemaType.Array)
                {
                    var itemTypeNames = ConvertTypeToNames(itemsSchema);
                    // Only return single type arrays for simplicity.
                    if (itemTypeNames.Count == 1)
                    {
                        return $"array<{itemTypeNames[0]}>";
                    }
                }
            }
            return TypeArray;
        }
    }
}