| File: Utilities\AIJsonUtilities.Schema.Transform.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. using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; public static partial class AIJsonUtilities { /// <summary> /// Transforms the given JSON schema based on the provided options. /// </summary> /// <param name="schema">The schema document to transform.</param> /// <param name="transformOptions">The options governing schema transformation.</param> /// <returns>A new schema document with transformations applied.</returns> /// <remarks>The schema and any nested schemas are transformed using depth-first traversal.</remarks> public static JsonElement TransformSchema(JsonElement schema, AIJsonSchemaTransformOptions transformOptions) { _ = Throw.IfNull(transformOptions); if (transformOptions == AIJsonSchemaTransformOptions.Default) { Throw.ArgumentException(nameof(transformOptions), "The options instance does not specify any transformations."); } JsonNode? nodeSchema = JsonSerializer.SerializeToNode(schema, JsonContext.Default.JsonElement); JsonNode transformedSchema = TransformSchema(nodeSchema, transformOptions); return JsonSerializer.SerializeToElement(transformedSchema, JsonContextNoIndentation.Default.JsonNode); } private static JsonNode TransformSchema(JsonNode? schema, AIJsonSchemaTransformOptions transformOptions) { List<string>? path = transformOptions.TransformSchemaNode is not null ? [] : null; return TransformSchemaCore(schema, transformOptions, path); } private static JsonNode TransformSchemaCore(JsonNode? schema, AIJsonSchemaTransformOptions transformOptions, List<string>? path) { switch (schema?.GetValueKind()) { case JsonValueKind.False: if (transformOptions.ConvertBooleanSchemas) { schema = new JsonObject { [NotPropertyName] = (JsonNode)true }; } break; case JsonValueKind.True: if (transformOptions.ConvertBooleanSchemas) { schema = new JsonObject(); } break; case JsonValueKind.Object: JsonObject schemaObj = (JsonObject)schema; JsonObject? properties = null; // Step 1. Recursively apply transformations to any nested schemas we might be able to detect. if (schemaObj.TryGetPropertyValue(PropertiesPropertyName, out JsonNode? props) && props is JsonObject propsObj) { properties = propsObj; path?.Add(PropertiesPropertyName); foreach (var prop in properties.ToArray()) { path?.Add(prop.Key); properties[prop.Key] = TransformSchemaCore(prop.Value, transformOptions, path); path?.RemoveAt(path.Count - 1); } path?.RemoveAt(path.Count - 1); } if (schemaObj.TryGetPropertyValue(ItemsPropertyName, out JsonNode? itemsSchema)) { path?.Add(ItemsPropertyName); schemaObj[ItemsPropertyName] = TransformSchemaCore(itemsSchema, transformOptions, path); path?.RemoveAt(path.Count - 1); } if (schemaObj.TryGetPropertyValue(AdditionalPropertiesPropertyName, out JsonNode? additionalProps) && additionalProps?.GetValueKind() is not JsonValueKind.False) { path?.Add(AdditionalPropertiesPropertyName); schemaObj[AdditionalPropertiesPropertyName] = TransformSchemaCore(additionalProps, transformOptions, path); path?.RemoveAt(path.Count - 1); } if (schemaObj.TryGetPropertyValue(NotPropertyName, out JsonNode? notSchema)) { path?.Add(NotPropertyName); schemaObj[NotPropertyName] = TransformSchemaCore(notSchema, transformOptions, path); path?.RemoveAt(path.Count - 1); } // Traverse keywords that contain arrays of schemas ReadOnlySpan<string> combinatorKeywords = ["anyOf", "oneOf", "allOf"]; foreach (string combinatorKeyword in combinatorKeywords) { if (schemaObj.TryGetPropertyValue(combinatorKeyword, out JsonNode? combinatorSchema) && combinatorSchema is JsonArray combinatorArray) { path?.Add(combinatorKeyword); for (int i = 0; i < combinatorArray.Count; i++) { path?.Add($"[{i}]"); JsonNode element = TransformSchemaCore(combinatorArray[i], transformOptions, path); if (!ReferenceEquals(element, combinatorArray[i])) { combinatorArray[i] = element; } path?.RemoveAt(path.Count - 1); } path?.RemoveAt(path.Count - 1); } } // Step 2. Apply node-level transformations per the settings. if (transformOptions.DisallowAdditionalProperties && properties is not null && !schemaObj.ContainsKey(AdditionalPropertiesPropertyName)) { schemaObj[AdditionalPropertiesPropertyName] = (JsonNode)false; } if (transformOptions.RequireAllProperties && properties is not null) { JsonArray requiredProps = []; foreach (var prop in properties) { requiredProps.Add((JsonNode)prop.Key); } schemaObj[RequiredPropertyName] = requiredProps; } if (transformOptions.UseNullableKeyword && schemaObj.TryGetPropertyValue(TypePropertyName, out JsonNode? typeSchema) && typeSchema is JsonArray typeArray) { bool isNullable = false; string? foundType = null; foreach (JsonNode? typeNode in typeArray) { string typeString = (string)typeNode!; if (typeString is "null") { isNullable = true; continue; } if (foundType is not null) { // The array contains more than one non-null types, abort the transformation. foundType = null; break; } foundType = typeString; } if (isNullable && foundType is not null) { schemaObj["type"] = (JsonNode)foundType; schemaObj["nullable"] = (JsonNode)true; } } if (transformOptions.MoveDefaultKeywordToDescription && schemaObj.TryGetPropertyValue(DefaultPropertyName, out JsonNode? defaultSchema)) { string? description = schemaObj.TryGetPropertyValue(DescriptionPropertyName, out JsonNode? descriptionSchema) ? descriptionSchema?.GetValue<string>() : null; string defaultValueJson = JsonSerializer.Serialize(defaultSchema, JsonContextNoIndentation.Default.JsonNode!); description = description is null ? $"Default value: {defaultValueJson}" : $"{description} (Default value: {defaultValueJson})"; schemaObj[DescriptionPropertyName] = description; _ = schemaObj.Remove(DefaultPropertyName); } break; default: Throw.ArgumentException(nameof(schema), "Schema must be an object or a boolean value."); break; } // Apply user-defined transformations as the final step. if (transformOptions.TransformSchemaNode is { } transformer) { Debug.Assert(path != null, "Path should not be null when TransformSchemaNode is provided."); schema = transformer(new AIJsonSchemaTransformContext(path!.ToArray()), schema); } return schema; } }