| File: Extensions\JsonTypeInfoExtensions.cs | Web Access |
| Project: src\aspnetcore\src\OpenApi\src\Microsoft.AspNetCore.OpenApi.csproj (Microsoft.AspNetCore.OpenApi) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.IO.Pipelines; using System.Linq; using System.Text.Json; using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.OpenApi; internal static class JsonTypeInfoExtensions { private static readonly Dictionary<Type, string> _simpleTypeToName = new() { [typeof(bool)] = "boolean", [typeof(byte)] = "byte", [typeof(int)] = "int", [typeof(uint)] = "uint", [typeof(long)] = "long", [typeof(ulong)] = "ulong", [typeof(short)] = "short", [typeof(ushort)] = "ushort", [typeof(float)] = "float", [typeof(double)] = "double", [typeof(decimal)] = "decimal", [typeof(DateTime)] = "DateTime", [typeof(DateTimeOffset)] = "DateTimeOffset", [typeof(Guid)] = "Guid", [typeof(char)] = "char", [typeof(Uri)] = "Uri", [typeof(string)] = "string", [typeof(IFormFile)] = "IFormFile", [typeof(IFormFileCollection)] = "IFormFileCollection", [typeof(PipeReader)] = "PipeReader", [typeof(Stream)] = "Stream" }; /// <summary> /// The following method maps a JSON type to a schema reference ID that will eventually be used as the /// schema reference name in the OpenAPI document. These schema reference names are considered URL fragments /// in the context of JSON Schema's $ref keyword and must comply with the character restrictions of URL fragments. /// In particular, the generated strings can contain alphanumeric characters and a subset of special symbols. This /// means that certain symbols that appear commonly in .NET type names like ">" are not permitted in the /// generated reference ID. /// </summary> /// <param name="jsonTypeInfo">The <see cref="JsonTypeInfo"/> associated with the target schema.</param> /// <param name="isTopLevel"> /// When <see langword="false" />, returns schema name for primitive /// types to support use in list/dictionary types. /// </param> /// <returns>The schema reference ID represented as a string name.</returns> internal static string? GetSchemaReferenceId(this JsonTypeInfo jsonTypeInfo, bool isTopLevel = true) { var type = jsonTypeInfo.Type; var underlyingType = Nullable.GetUnderlyingType(type); if (isTopLevel && OpenApiConstants.PrimitiveTypes.Contains(underlyingType ?? type)) { return null; } // Short-hand if the type we're generating a schema reference ID for is // one of the simple types defined above. if (_simpleTypeToName.TryGetValue(type, out var simpleName)) { return simpleName; } // Use the same JSON Patch schema for all JSON Patch document types (JsonPatchDocument, // JsonPatchDocument<T>, derived types, etc.) as otherwise we'll generate a schema // per unique type which are otherwise identical to each other. if (type.IsJsonPatchDocument()) { return "JsonPatchDocument"; } // Although arrays are enumerable types they are not encoded correctly // with JsonTypeInfoKind.Enumerable so we handle the Enumerable type // case here. if (jsonTypeInfo is JsonTypeInfo { Kind: JsonTypeInfoKind.Enumerable } || type.IsArray) { return null; } if (jsonTypeInfo is JsonTypeInfo { Kind: JsonTypeInfoKind.Dictionary }) { return null; } return type.GetSchemaReferenceId(jsonTypeInfo.Options); } internal static string GetSchemaReferenceId(this Type type, JsonSerializerOptions options) { // Check the simple types map first to account for the element types // of enumerables that have been processed above. if (_simpleTypeToName.TryGetValue(type, out var simpleName)) { return simpleName; } // Special handling for anonymous types if (type.Name.StartsWith("<>f", StringComparison.Ordinal)) { var typeName = "AnonymousType"; var anonymousTypeProperties = type.GetGenericArguments(); var propertyNames = string.Join("And", anonymousTypeProperties.Select(p => p.GetSchemaReferenceId(options))); return $"{typeName}Of{propertyNames}"; } // Special handling for nullable value types if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)) { return type.GetGenericArguments()[0].GetSchemaReferenceId(options); } // Special handling for array types whose names contain '[]', which is not // valid in OpenAPI schema keys (must match ^[a-zA-Z0-9._-]+$). if (type.IsArray) { var elementType = type.GetElementType()!; return $"ArrayOf{elementType.GetSchemaReferenceId(options)}"; } // Special handling for generic types that are collections // Generic types become a concatenation of the generic type name and the type arguments if (type.IsGenericType) { // We need to handle the case where the generic type is a nested type, // so we check if the name contains a backtick already. // For more information: https://github.com/dotnet/aspnetcore/issues/59092 var backtickIndex = type.Name.LastIndexOf('`'); var isNestedGenericType = backtickIndex == -1; var genericTypeName = isNestedGenericType ? type.Name : type.Name[..backtickIndex]; var genericArguments = type.GetGenericArguments(); var argumentNames = string.Join("And", genericArguments.Select(arg => arg.GetSchemaReferenceId(options))); return $"{genericTypeName}Of{argumentNames}"; } return type.Name; } }