diff --git a/eng/Versions.props b/eng/Versions.props index 47d338c5f21c48..a5d5d2971a27f3 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -186,6 +186,8 @@ 3.12.0 4.5.0 6.0.0 + 5.0.0 + 7.0.2 13.0.3 1.0.2 2.0.4 diff --git a/src/libraries/System.Text.Json/ref/System.Text.Json.cs b/src/libraries/System.Text.Json/ref/System.Text.Json.cs index 2dbe6cad3e5ebd..6905b8d1e5dd52 100644 --- a/src/libraries/System.Text.Json/ref/System.Text.Json.cs +++ b/src/libraries/System.Text.Json/ref/System.Text.Json.cs @@ -888,6 +888,29 @@ internal JsonValue() { } public abstract bool TryGetValue([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out T? value); } } +namespace System.Text.Json.Schema +{ + public static partial class JsonSchemaExporter + { + public static System.Text.Json.Nodes.JsonNode GetJsonSchemaAsNode(this System.Text.Json.JsonSerializerOptions options, System.Type type, System.Text.Json.Schema.JsonSchemaExporterOptions? exporterOptions = null) { throw null; } + public static System.Text.Json.Nodes.JsonNode GetJsonSchemaAsNode(this System.Text.Json.Serialization.Metadata.JsonTypeInfo typeInfo, System.Text.Json.Schema.JsonSchemaExporterOptions? exporterOptions = null) { throw null; } + } + public readonly partial struct JsonSchemaExporterContext + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public System.Text.Json.Serialization.Metadata.JsonPropertyInfo? PropertyInfo { get { throw null; } } + public System.ReadOnlySpan Path { get { throw null; } } + public System.Text.Json.Serialization.Metadata.JsonTypeInfo TypeInfo { get { throw null; } } + } + public sealed partial class JsonSchemaExporterOptions + { + public JsonSchemaExporterOptions() { } + public static System.Text.Json.Schema.JsonSchemaExporterOptions Default { get { throw null; } } + public System.Func? TransformSchemaNode { get { throw null; } init { } } + public bool TreatNullObliviousAsNonNullable { get { throw null; } init { } } + } +} namespace System.Text.Json.Serialization { public partial interface IJsonOnDeserialized diff --git a/src/libraries/System.Text.Json/src/Resources/Strings.resx b/src/libraries/System.Text.Json/src/Resources/Strings.resx index 19ebcbd47137ac..4ff132432c2145 100644 --- a/src/libraries/System.Text.Json/src/Resources/Strings.resx +++ b/src/libraries/System.Text.Json/src/Resources/Strings.resx @@ -752,4 +752,10 @@ NullabilityInfoContext is not supported in the current application because 'System.Reflection.NullabilityInfoContext.IsSupported' is set to false. Set the MSBuild Property 'NullabilityInfoContextSupport' to true in order to enable it. + + JSON schema generation is not supported for contracts using ReferenceHandler.Preserve. + + + The depth of the generated JSON schema exceeds the JsonSerializerOptions.MaxDepth setting. + diff --git a/src/libraries/System.Text.Json/src/System.Text.Json.csproj b/src/libraries/System.Text.Json/src/System.Text.Json.csproj index f7bbf8f386f1eb..ac931995581649 100644 --- a/src/libraries/System.Text.Json/src/System.Text.Json.csproj +++ b/src/libraries/System.Text.Json/src/System.Text.Json.csproj @@ -100,6 +100,11 @@ The System.Text.Json library is built-in as part of the shared framework in .NET + + + + + diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchema.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchema.cs new file mode 100644 index 00000000000000..d261b374b390a7 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchema.cs @@ -0,0 +1,324 @@ +// 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 False { get; } = new(false); + public static JsonSchema True { get; } = 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? 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>? Properties { get => _properties; set { VerifyMutable(); _properties = value; } } + private List>? _properties; + + public List? Required { get => _required; set { VerifyMutable(); _required = value; } } + private List? _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? AnyOf { get => _anyOf; set { VerifyMutable(); _anyOf = value; } } + private List? _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) + { + 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) + { + 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 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; + } + } + + private static ReadOnlySpan 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, + }; + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs new file mode 100644 index 00000000000000..a4f9cd3594a471 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporter.cs @@ -0,0 +1,510 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; + +namespace System.Text.Json.Schema +{ + /// + /// Functionality for exporting JSON schema from serialization contracts defined in . + /// + public static class JsonSchemaExporter + { + /// + /// Gets the JSON schema for as a document. + /// + /// The options declaring the contract for the type. + /// The type for which to resolve a schema. + /// The options object governing the export operation. + /// A JSON object containing the schema for . + public static JsonNode GetJsonSchemaAsNode(this JsonSerializerOptions options, Type type, JsonSchemaExporterOptions? exporterOptions = null) + { + if (options is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(options)); + } + + if (type is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(type)); + } + + ValidateOptions(options); + JsonTypeInfo typeInfo = options.GetTypeInfoInternal(type); + return typeInfo.GetJsonSchemaAsNode(exporterOptions); + } + + /// + /// Gets the JSON schema for as a document. + /// + /// The contract from which to resolve the JSON schema. + /// The options object governing the export operation. + /// A JSON object containing the schema for . + public static JsonNode GetJsonSchemaAsNode(this JsonTypeInfo typeInfo, JsonSchemaExporterOptions? exporterOptions = null) + { + if (typeInfo is null) + { + ThrowHelper.ThrowArgumentNullException(nameof(typeInfo)); + } + + ValidateOptions(typeInfo.Options); + exporterOptions ??= JsonSchemaExporterOptions.Default; + + typeInfo.EnsureConfigured(); + GenerationState state = new(typeInfo.Options, exporterOptions); + JsonSchema schema = MapJsonSchemaCore(ref state, typeInfo); + return schema.ToJsonNode(exporterOptions); + } + + private static JsonSchema MapJsonSchemaCore( + ref GenerationState state, + JsonTypeInfo typeInfo, + JsonPropertyInfo? propertyInfo = null, + JsonConverter? customConverter = null, + JsonNumberHandling? customNumberHandling = null, + Type? parentPolymorphicType = null, + bool parentPolymorphicTypeContainsTypesWithoutDiscriminator = false, + bool parentPolymorphicTypeIsNonNullable = false, + KeyValuePair? typeDiscriminator = null, + bool cacheResult = true) + { + Debug.Assert(typeInfo.IsConfigured); + + if (cacheResult && state.TryPushType(typeInfo, propertyInfo, out string? existingJsonPointer)) + { + // We're generating the schema of a recursive type, return a reference pointing to the outermost schema. + return CompleteSchema(ref state, new JsonSchema { Ref = existingJsonPointer }); + } + + JsonConverter effectiveConverter = customConverter ?? typeInfo.Converter; + JsonNumberHandling effectiveNumberHandling = customNumberHandling ?? typeInfo.NumberHandling ?? typeInfo.Options.NumberHandling; + if (effectiveConverter.GetSchema(effectiveNumberHandling) is { } schema) + { + // A schema has been provided by the converter. + return CompleteSchema(ref state, schema); + } + + if (parentPolymorphicType is null && typeInfo.PolymorphismOptions is { DerivedTypes.Count: > 0 } polyOptions) + { + // This is the base type of a polymorphic type hierarchy. The schema for this type + // will include an "anyOf" property with the schemas for all derived types. + string typeDiscriminatorKey = polyOptions.TypeDiscriminatorPropertyName; + List derivedTypes = new(polyOptions.DerivedTypes); + + if (!typeInfo.Type.IsAbstract && !IsPolymorphicTypeThatSpecifiesItselfAsDerivedType(typeInfo)) + { + // For non-abstract base types that haven't been explicitly configured, + // add a trivial schema to the derived types since we should support it. + derivedTypes.Add(new JsonDerivedType(typeInfo.Type)); + } + + bool containsTypesWithoutDiscriminator = derivedTypes.Exists(static derivedTypes => derivedTypes.TypeDiscriminator is null); + JsonSchemaType schemaType = JsonSchemaType.Any; + List? anyOf = new(derivedTypes.Count); + + state.PushSchemaNode(JsonSchema.AnyOfPropertyName); + + foreach (JsonDerivedType derivedType in derivedTypes) + { + Debug.Assert(derivedType.TypeDiscriminator is null or int or string); + + KeyValuePair? derivedTypeDiscriminator = null; + if (derivedType.TypeDiscriminator is { } discriminatorValue) + { + JsonNode discriminatorNode = discriminatorValue switch + { + string stringId => (JsonNode)stringId, + _ => (JsonNode)(int)discriminatorValue, + }; + + JsonSchema discriminatorSchema = new() { Constant = discriminatorNode }; + derivedTypeDiscriminator = new(typeDiscriminatorKey, discriminatorSchema); + } + + JsonTypeInfo derivedTypeInfo = typeInfo.Options.GetTypeInfoInternal(derivedType.DerivedType); + + state.PushSchemaNode(anyOf.Count.ToString(CultureInfo.InvariantCulture)); + JsonSchema derivedSchema = MapJsonSchemaCore( + ref state, + derivedTypeInfo, + parentPolymorphicType: typeInfo.Type, + typeDiscriminator: derivedTypeDiscriminator, + parentPolymorphicTypeContainsTypesWithoutDiscriminator: containsTypesWithoutDiscriminator, + parentPolymorphicTypeIsNonNullable: propertyInfo is { IsGetNullable: false, IsSetNullable: false }, + cacheResult: false); + + state.PopSchemaNode(); + + // Determine if all derived schemas have the same type. + if (anyOf.Count == 0) + { + schemaType = derivedSchema.Type; + } + else if (schemaType != derivedSchema.Type) + { + schemaType = JsonSchemaType.Any; + } + + anyOf.Add(derivedSchema); + } + + state.PopSchemaNode(); + + if (schemaType is not JsonSchemaType.Any) + { + // If all derived types have the same schema type, we can simplify the schema + // by moving the type keyword to the base schema and removing it from the derived schemas. + foreach (JsonSchema derivedSchema in anyOf) + { + derivedSchema.Type = JsonSchemaType.Any; + + if (derivedSchema.KeywordCount == 0) + { + // if removing the type results in an empty schema, + // remove the anyOf array entirely since it's always true. + anyOf = null; + break; + } + } + } + + return CompleteSchema(ref state, new() + { + Type = schemaType, + AnyOf = anyOf, + // If all derived types have a discriminator, we can require it in the base schema. + Required = containsTypesWithoutDiscriminator ? null : [typeDiscriminatorKey] + }); + } + + if (effectiveConverter.NullableElementConverter is { } elementConverter) + { + JsonTypeInfo elementTypeInfo = typeInfo.Options.GetTypeInfo(elementConverter.Type!); + schema = MapJsonSchemaCore(ref state, elementTypeInfo, customConverter: elementConverter, cacheResult: false); + + if (schema.Enum != null) + { + Debug.Assert(elementTypeInfo.Type.IsEnum, "The enum keyword should only be populated by schemas for enum types."); + schema.Enum.Add(null); // Append null to the enum array. + } + + return CompleteSchema(ref state, schema); + } + + switch (typeInfo.Kind) + { + case JsonTypeInfoKind.Object: + List>? properties = null; + List? required = null; + JsonSchema? additionalProperties = null; + + if (typeInfo.UnmappedMemberHandling is JsonUnmappedMemberHandling.Disallow) + { + additionalProperties = JsonSchema.False; + } + + if (typeDiscriminator is { } typeDiscriminatorPair) + { + (properties ??= []).Add(typeDiscriminatorPair); + if (parentPolymorphicTypeContainsTypesWithoutDiscriminator) + { + // Require the discriminator here since it's not common to all derived types. + (required ??= []).Add(typeDiscriminatorPair.Key); + } + } + + state.PushSchemaNode(JsonSchema.PropertiesPropertyName); + foreach (JsonPropertyInfo property in typeInfo.Properties) + { + if (property is { Get: null, Set: null } or { IsExtensionData: true }) + { + continue; // Skip JsonIgnored properties and extension data + } + + state.PushSchemaNode(property.Name); + JsonSchema propertySchema = MapJsonSchemaCore( + ref state, + property.JsonTypeInfo, + propertyInfo: property, + customConverter: property.EffectiveConverter, + customNumberHandling: property.EffectiveNumberHandling); + + state.PopSchemaNode(); + + if (property.AssociatedParameter is { HasDefaultValue: true } parameterInfo) + { + propertySchema.DefaultValue = JsonSerializer.SerializeToNode(parameterInfo.DefaultValue, property.JsonTypeInfo); + propertySchema.HasDefaultValue = true; + } + + (properties ??= []).Add(new(property.Name, propertySchema)); + + // Mark as required if either the property is required or the associated constructor parameter is non-optional. + // While the latter implies the former in cases where the JsonSerializerOptions.RespectRequiredConstructorParameters + // setting has been enabled, for the case of the schema exporter we always mark non-optional constructor parameters as required. + if (property is { IsRequired: true } or { AssociatedParameter.IsRequiredParameter: true }) + { + (required ??= []).Add(property.Name); + } + } + + state.PopSchemaNode(); + return CompleteSchema(ref state, new() + { + Type = JsonSchemaType.Object, + Properties = properties, + Required = required, + AdditionalProperties = additionalProperties, + }); + + case JsonTypeInfoKind.Enumerable: + Debug.Assert(typeInfo.ElementTypeInfo != null); + + if (typeDiscriminator is null) + { + state.PushSchemaNode(JsonSchema.ItemsPropertyName); + JsonSchema items = MapJsonSchemaCore(ref state, typeInfo.ElementTypeInfo, customNumberHandling: effectiveNumberHandling); + state.PopSchemaNode(); + + return CompleteSchema(ref state, new() + { + Type = JsonSchemaType.Array, + Items = items.IsTrue ? null : items, + }); + } + else + { + // Polymorphic enumerable types are represented using a wrapping object: + // { "$type" : "discriminator", "$values" : [element1, element2, ...] } + // Which corresponds to the schema + // { "properties" : { "$type" : { "const" : "discriminator" }, "$values" : { "type" : "array", "items" : { ... } } } } + const string ValuesKeyword = JsonSerializer.ValuesPropertyName; + + state.PushSchemaNode(JsonSchema.PropertiesPropertyName); + state.PushSchemaNode(ValuesKeyword); + state.PushSchemaNode(JsonSchema.ItemsPropertyName); + + JsonSchema items = MapJsonSchemaCore(ref state, typeInfo.ElementTypeInfo, customNumberHandling: effectiveNumberHandling); + + state.PopSchemaNode(); + state.PopSchemaNode(); + state.PopSchemaNode(); + + return CompleteSchema(ref state, new() + { + Type = JsonSchemaType.Object, + Properties = + [ + typeDiscriminator.Value, + new(ValuesKeyword, + new JsonSchema() + { + Type = JsonSchemaType.Array, + Items = items.IsTrue ? null : items, + }), + ], + Required = parentPolymorphicTypeContainsTypesWithoutDiscriminator ? [typeDiscriminator.Value.Key] : null, + }); + } + + case JsonTypeInfoKind.Dictionary: + Debug.Assert(typeInfo.ElementTypeInfo != null); + + List>? dictProps = null; + List? dictRequired = null; + + if (typeDiscriminator is { } dictDiscriminator) + { + dictProps = [dictDiscriminator]; + if (parentPolymorphicTypeContainsTypesWithoutDiscriminator) + { + // Require the discriminator here since it's not common to all derived types. + dictRequired = [dictDiscriminator.Key]; + } + } + + state.PushSchemaNode(JsonSchema.AdditionalPropertiesPropertyName); + JsonSchema valueSchema = MapJsonSchemaCore(ref state, typeInfo.ElementTypeInfo, customNumberHandling: effectiveNumberHandling); + state.PopSchemaNode(); + + return CompleteSchema(ref state, new() + { + Type = JsonSchemaType.Object, + Properties = dictProps, + Required = dictRequired, + AdditionalProperties = valueSchema.IsTrue ? null : valueSchema, + }); + + default: + Debug.Assert(typeInfo.Kind is JsonTypeInfoKind.None); + // Return a `true` schema for types with user-defined converters. + return CompleteSchema(ref state, JsonSchema.True); + } + + JsonSchema CompleteSchema(ref GenerationState state, JsonSchema schema) + { + if (schema.Ref is null) + { + // A schema is marked as nullable if either + // 1. We have a schema for a property where either the getter or setter are marked as nullable. + // 2. We have a schema for a reference type, unless we're explicitly treating null-oblivious types as non-nullable. + bool isNullableSchema = propertyInfo != null + ? propertyInfo.IsGetNullable || propertyInfo.IsSetNullable + : typeInfo.CanBeNull && !parentPolymorphicTypeIsNonNullable && !state.ExporterOptions.TreatNullObliviousAsNonNullable; + + if (isNullableSchema) + { + schema.MakeNullable(); + } + + if (cacheResult) + { + state.PopGeneratedType(); + } + } + + if (state.ExporterOptions.TransformSchemaNode != null) + { + // Prime the schema for invocation by the JsonNode transformer. + schema.ExporterContext = state.CreateContext(typeInfo, propertyInfo); + } + + return schema; + } + } + + private static void ValidateOptions(JsonSerializerOptions options) + { + if (options.ReferenceHandler == ReferenceHandler.Preserve) + { + ThrowHelper.ThrowNotSupportedException_JsonSchemaExporterDoesNotSupportReferenceHandlerPreserve(); + } + + options.MakeReadOnly(); + } + + private static bool IsPolymorphicTypeThatSpecifiesItselfAsDerivedType(JsonTypeInfo typeInfo) + { + Debug.Assert(typeInfo.PolymorphismOptions is not null); + + foreach (JsonDerivedType derivedType in typeInfo.PolymorphismOptions.DerivedTypes) + { + if (derivedType.DerivedType == typeInfo.Type) + { + return true; + } + } + + return false; + } + + private readonly ref struct GenerationState(JsonSerializerOptions options, JsonSchemaExporterOptions exporterOptions) + { + private readonly List _currentPath = []; + private readonly List<(JsonTypeInfo typeInfo, JsonPropertyInfo? propertyInfo, int depth)> _generationStack = []; + + public int CurrentDepth => _currentPath.Count; + public JsonSerializerOptions Options { get; } = options; + public JsonSchemaExporterOptions ExporterOptions { get; } = exporterOptions; + + public void PushSchemaNode(string nodeId) + { + if (CurrentDepth == Options.EffectiveMaxDepth) + { + ThrowHelper.ThrowInvalidOperationException_JsonSchemaExporterDepthTooLarge(); + } + + _currentPath.Add(nodeId); + } + + public void PopSchemaNode() + { + Debug.Assert(CurrentDepth > 0); + _currentPath.RemoveAt(_currentPath.Count - 1); + } + + /// + /// Pushes the current type/property to the generation stack or returns a JSON pointer if the type is recursive. + /// + public bool TryPushType(JsonTypeInfo typeInfo, JsonPropertyInfo? propertyInfo, [NotNullWhen(true)] out string? existingJsonPointer) + { + foreach ((JsonTypeInfo otherTypeInfo, JsonPropertyInfo? otherPropertyInfo, int depth) in _generationStack) + { + if (typeInfo == otherTypeInfo && propertyInfo == otherPropertyInfo) + { + existingJsonPointer = FormatJsonPointer(_currentPath, depth); + return true; + } + } + + _generationStack.Add((typeInfo, propertyInfo, CurrentDepth)); + existingJsonPointer = null; + return false; + } + + public void PopGeneratedType() + { + Debug.Assert(_generationStack.Count > 0); + _generationStack.RemoveAt(_generationStack.Count - 1); + } + + public JsonSchemaExporterContext CreateContext(JsonTypeInfo typeInfo, JsonPropertyInfo? propertyInfo) + { + return new JsonSchemaExporterContext(typeInfo, propertyInfo, _currentPath.ToArray()); + } + + private static string FormatJsonPointer(List currentPathList, int depth) + { + Debug.Assert(0 <= depth && depth < currentPathList.Count); + + if (depth == 0) + { + return "#"; + } + + using ValueStringBuilder sb = new(initialCapacity: depth * 10); + sb.Append('#'); + + for (int i = 0; i < depth; i++) + { + ReadOnlySpan segment = currentPathList[i].AsSpan(); + sb.Append('/'); + + do + { + // Per RFC 6901 the characters '~' and '/' must be escaped. + int pos = segment.IndexOfAny('~', '/'); + if (pos < 0) + { + sb.Append(segment); + break; + } + + sb.Append(segment.Slice(0, pos)); + + if (segment[pos] == '~') + { + sb.Append("~0"); + } + else + { + Debug.Assert(segment[pos] == '/'); + sb.Append("~1"); + } + + segment = segment.Slice(pos + 1); + } + while (!segment.IsEmpty); + } + + return sb.ToString(); + } + } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporterContext.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporterContext.cs new file mode 100644 index 00000000000000..f8143e347656cf --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporterContext.cs @@ -0,0 +1,37 @@ +// 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.Serialization.Metadata; + +namespace System.Text.Json.Schema +{ + /// + /// Defines the context for the generated JSON schema for a particular node in a type graph. + /// + public readonly struct JsonSchemaExporterContext + { + private readonly string[] _path; + + internal JsonSchemaExporterContext(JsonTypeInfo typeInfo, JsonPropertyInfo? propertyInfo, string[] path) + { + TypeInfo = typeInfo; + PropertyInfo = propertyInfo; + _path = path; + } + + /// + /// The for the type being processed. + /// + public JsonTypeInfo TypeInfo { get; } + + /// + /// The if the schema is being generated for a property. + /// + public JsonPropertyInfo? PropertyInfo { get; } + + /// + /// The path to the current node in the generated JSON schema. + /// + public ReadOnlySpan Path => _path; + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporterOptions.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporterOptions.cs new file mode 100644 index 00000000000000..1567ad9aded1ed --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaExporterOptions.cs @@ -0,0 +1,33 @@ +// 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; + +namespace System.Text.Json.Schema +{ + /// + /// Configures the behavior of the APIs. + /// + public sealed class JsonSchemaExporterOptions + { + /// + /// Gets the default configuration object used by . + /// + public static JsonSchemaExporterOptions Default { get; } = new(); + + /// + /// Determines whether non-nullable schemas should be generated for null oblivious reference types. + /// + /// + /// Defaults to . Due to restrictions in the run-time representation of nullable reference types + /// most occurences are null oblivious and are treated as nullable by the serializer. A notable exception to that rule + /// are nullability annotations of field, property and constructor parameters which are represented in the contract metadata. + /// + public bool TreatNullObliviousAsNonNullable { get; init; } + + /// + /// Defines a callback that is invoked for every schema that is generated within the type graph. + /// + public Func? TransformSchemaNode { get; init; } + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaType.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaType.cs new file mode 100644 index 00000000000000..f0b68656332269 --- /dev/null +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Schema/JsonSchemaType.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Text.Json.Schema +{ + [Flags] + internal enum JsonSchemaType + { + Any = 0, + Null = 1, + Boolean = 2, + Integer = 4, + Number = 8, + String = 16, + Object = 32, + Array = 64, + } +} diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/CastingConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/CastingConverter.cs index fabf97436bd267..a49efe33765b0d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/CastingConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/CastingConverter.cs @@ -3,7 +3,9 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Nodes; using System.Text.Json.Reflection; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -72,5 +74,8 @@ internal override T ReadNumberWithCustomHandling(ref Utf8JsonReader reader, Json internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, T? value, JsonNumberHandling handling) => _sourceConverter.WriteNumberWithCustomHandlingAsObject(writer, value, handling); + + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) + => _sourceConverter.GetSchema(numberHandling); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs index 7808ca49443de8..6cb9f90741fa11 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/JsonMetadataServicesConverter.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Serialization.Converters @@ -65,5 +67,8 @@ internal override bool OnTryWrite(Utf8JsonWriter writer, T value, JsonSerializer internal override void ConfigureJsonTypeInfo(JsonTypeInfo jsonTypeInfo, JsonSerializerOptions options) => Converter.ConfigureJsonTypeInfo(jsonTypeInfo, options); + + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) + => Converter.GetSchema(numberHandling); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonArrayConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonArrayConverter.cs index 0410d22df1f382..f30821770a1824 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonArrayConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonArrayConverter.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -38,5 +39,7 @@ public static JsonArray ReadList(ref Utf8JsonReader reader, JsonNodeOptions? opt JsonElement jElement = JsonElement.ParseValue(ref reader); return new JsonArray(jElement, options); } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => new() { Type = JsonSchemaType.Array }; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverter.cs index 89caeaa08f50e4..3728ab26ad6f36 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonNodeConverter.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Text.Json.Nodes; +using System.Text.Json.Schema; using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Serialization.Converters @@ -78,5 +79,7 @@ public override void Write(Utf8JsonWriter writer, JsonNode? value, JsonSerialize return node; } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => JsonSchema.True; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonObjectConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonObjectConverter.cs index 4455e92916eb0b..44f81126d7a65d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonObjectConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonObjectConverter.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Text.Json.Nodes; +using System.Text.Json.Schema; using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Serialization.Converters @@ -64,5 +65,7 @@ public static JsonObject ReadObject(ref Utf8JsonReader reader, JsonNodeOptions? JsonObject jObject = new JsonObject(jElement, options); return jObject; } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => new() { Type = JsonSchemaType.Object }; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValueConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValueConverter.cs index 6b09c360987dd0..51253ddd70c82a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValueConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Node/JsonValueConverter.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Text.Json.Nodes; +using System.Text.Json.Schema; using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Serialization.Converters @@ -30,5 +31,7 @@ public override void Write(Utf8JsonWriter writer, JsonValue? value, JsonSerializ JsonValue value = new JsonValuePrimitive(element, JsonMetadataServices.JsonElementConverter, options.GetNodeOptions()); return value; } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => JsonSchema.True; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverter.cs index 4511e150b37eed..bda21c258fbe0e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Object/ObjectConverter.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Text.Json.Nodes; +using System.Text.Json.Schema; using System.Text.Json.Serialization.Metadata; namespace System.Text.Json.Serialization.Converters @@ -145,5 +146,7 @@ internal override bool OnTryRead(ref Utf8JsonReader reader, Type typeToConvert, return true; } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => JsonSchema.True; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/BooleanConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/BooleanConverter.cs index 340a2105db8a8b..1d5dd93c3529cb 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/BooleanConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/BooleanConverter.cs @@ -3,6 +3,8 @@ using System.Buffers.Text; using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -35,5 +37,7 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, bool value { writer.WritePropertyName(value); } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => new() { Type = JsonSchemaType.Boolean }; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteArrayConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteArrayConverter.cs index 9b97a6d93e2163..3f0f871c81941c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteArrayConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteArrayConverter.cs @@ -1,6 +1,8 @@ // 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.Schema; + namespace System.Text.Json.Serialization.Converters { internal sealed class ByteArrayConverter : JsonConverter @@ -26,5 +28,7 @@ public override void Write(Utf8JsonWriter writer, byte[]? value, JsonSerializerO writer.WriteBase64StringValue(value); } } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => new() { Type = JsonSchemaType.String }; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteConverter.cs index 9df14e2060976c..8e03344ab7e434 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ByteConverter.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -54,5 +56,8 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, byte writer.WriteNumberValue(value); } } + + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) => + GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/CharConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/CharConverter.cs index c102a0879e1ac6..3bdca6204c6bee 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/CharConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/CharConverter.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Runtime.InteropServices; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -60,5 +60,8 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, char value #endif ); } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => + new() { Type = JsonSchemaType.String, MinLength = 1, MaxLength = 1 }; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateOnlyConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateOnlyConverter.cs index 9917f47fcf8db6..b780780fa4b89e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateOnlyConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateOnlyConverter.cs @@ -1,9 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Buffers; using System.Diagnostics; using System.Globalization; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -78,5 +78,7 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, DateOnly v Debug.Assert(formattedSuccessfully && charsWritten == FormatLength); writer.WritePropertyName(buffer); } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => new() { Type = JsonSchemaType.String, Format = "date" }; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateTimeConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateTimeConverter.cs index 204d39551cea29..f1423c2167218b 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateTimeConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateTimeConverter.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -27,5 +28,7 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, DateTime v { writer.WritePropertyName(value); } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => new JsonSchema { Type = JsonSchemaType.String, Format = "date-time" }; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateTimeOffsetConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateTimeOffsetConverter.cs index 6cfeaf4c38edcd..f0a2f527102c07 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateTimeOffsetConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DateTimeOffsetConverter.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -27,5 +28,7 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, DateTimeOf { writer.WritePropertyName(value); } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => new JsonSchema { Type = JsonSchemaType.String, Format = "date-time" }; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DecimalConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DecimalConverter.cs index 7b3ff9fade01d6..fffbed44d0b94d 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DecimalConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DecimalConverter.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -55,5 +56,8 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, deci writer.WriteNumberValue(value); } } + + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) => + GetSchemaForNumericType(JsonSchemaType.Number, numberHandling); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs index 0c6a6c4e26cc65..52cb8f4c0de4e4 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/DoubleConverter.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -65,5 +67,8 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, doub writer.WriteNumberValue(value); } } + + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) => + GetSchemaForNumericType(JsonSchemaType.Number, numberHandling, isIeeeFloatingPoint: true); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs index 5f9ddf1eee1b62..6ec71f05ddb38e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/EnumConverter.cs @@ -7,6 +7,8 @@ using System.Globalization; using System.Runtime.CompilerServices; using System.Text.Encodings.Web; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -17,6 +19,7 @@ internal sealed class EnumConverter : JsonPrimitiveConverter // Odd type codes are conveniently signed types (for enum backing types). private static readonly bool s_isSignedEnum = ((int)s_enumTypeCode % 2) == 1; + private static readonly bool s_isFlagsEnum = typeof(T).IsDefined(typeof(FlagsAttribute), inherit: false); private const string ValueSeparator = ", "; @@ -403,6 +406,39 @@ private bool TryParseEnumCore( return success; } + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) + { + if ((_converterOptions & EnumConverterOptions.AllowStrings) != 0) + { + // This explicitly ignores the integer component in converters configured as AllowNumbers | AllowStrings + // which is the default for JsonStringEnumConverter. This sacrifices some precision in the schema for simplicity. + + if (s_isFlagsEnum) + { + // Do not report enum values in case of flags. + return new() { Type = JsonSchemaType.String }; + } + + JsonNamingPolicy? namingPolicy = _namingPolicy; + JsonArray enumValues = []; +#if NET + string[] names = Enum.GetNames(); +#else + string[] names = Enum.GetNames(Type); +#endif + + for (int i = 0; i < names.Length; i++) + { + JsonNode name = FormatJsonName(names[i], namingPolicy); + enumValues.Add(name); + } + + return new() { Enum = enumValues }; + } + + return new() { Type = JsonSchemaType.Integer }; + } + private T ReadEnumUsingNamingPolicy(string? enumString) { if (_namingPolicy == null) diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/GuidConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/GuidConverter.cs index c808bc6d730ba3..cace588ea9be22 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/GuidConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/GuidConverter.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -27,5 +29,8 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, Guid value { writer.WritePropertyName(value); } + + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) => + new() { Type = JsonSchemaType.String, Format = "uuid" }; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs index c2bee78a5fa67d..458b00df093e55 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/HalfConverter.cs @@ -4,6 +4,8 @@ using System.Buffers; using System.Diagnostics; using System.Globalization; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -143,6 +145,9 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, Half } } + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) => + GetSchemaForNumericType(JsonSchemaType.Number, numberHandling, isIeeeFloatingPoint: true); + private static bool TryGetFloatingPointConstant(ref Utf8JsonReader reader, out Half value) { Span buffer = stackalloc byte[MaxFormatLength]; diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int128Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int128Converter.cs index da415abbb785c3..3dd7d9d863cd31 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int128Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int128Converter.cs @@ -4,6 +4,8 @@ using System.Buffers; using System.Diagnostics; using System.Globalization; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -128,6 +130,9 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, Int1 } } + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) => + GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling); + // Int128.TryParse(ROS) is not available on .NET 7, only Int128.TryParse(ROS). private static bool TryParse( #if NET8_0_OR_GREATER diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs index 73da887f71c922..cd5e897f1a996a 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int16Converter.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -57,5 +59,8 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, shor writer.WriteNumberValue((long)value); } } + + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) => + GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int32Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int32Converter.cs index a243b0d65f26e5..fc29970367f941 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int32Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int32Converter.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -57,5 +59,8 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, int writer.WriteNumberValue((long)value); } } + + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) => + GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int64Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int64Converter.cs index 7987daf7c6916c..7eae46d7ba4bdf 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int64Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/Int64Converter.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -55,5 +57,8 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, long writer.WriteNumberValue(value); } } + + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) => + GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonDocumentConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonDocumentConverter.cs index 3b70388658ec3c..fd964f09800f9e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonDocumentConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonDocumentConverter.cs @@ -1,6 +1,9 @@ // 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.Schema; +using System.Text.Json.Nodes; + namespace System.Text.Json.Serialization.Converters { internal sealed class JsonDocumentConverter : JsonConverter @@ -22,5 +25,7 @@ public override void Write(Utf8JsonWriter writer, JsonDocument? value, JsonSeria value.WriteTo(writer); } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => JsonSchema.True; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs index f371fe920a23c0..79e8ef4bf280d7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonElementConverter.cs @@ -1,6 +1,9 @@ // 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.Schema; +using System.Text.Json.Nodes; + namespace System.Text.Json.Serialization.Converters { internal sealed class JsonElementConverter : JsonConverter @@ -14,5 +17,7 @@ public override void Write(Utf8JsonWriter writer, JsonElement value, JsonSeriali { value.WriteTo(writer); } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => JsonSchema.True; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonPrimitiveConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonPrimitiveConverter.cs index 5d1ca58b76ae12..d7330ce675423e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonPrimitiveConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/JsonPrimitiveConverter.cs @@ -1,7 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -29,5 +32,40 @@ public sealed override T ReadAsPropertyName(ref Utf8JsonReader reader, Type type return ReadAsPropertyNameCore(ref reader, typeToConvert, options); } + + private protected static JsonSchema GetSchemaForNumericType(JsonSchemaType schemaType, JsonNumberHandling numberHandling, bool isIeeeFloatingPoint = false) + { + Debug.Assert(schemaType is JsonSchemaType.Integer or JsonSchemaType.Number); + Debug.Assert(!isIeeeFloatingPoint || schemaType is JsonSchemaType.Number); +#if NET + Debug.Assert(isIeeeFloatingPoint == (typeof(T) == typeof(double) || typeof(T) == typeof(float) || typeof(T) == typeof(Half))); +#endif + string? pattern = null; + + if ((numberHandling & (JsonNumberHandling.AllowReadingFromString | JsonNumberHandling.WriteAsString)) != 0) + { + pattern = schemaType is JsonSchemaType.Integer + ? @"^-?(?:0|[1-9]\d*)$" + : isIeeeFloatingPoint + ? @"^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$" + : @"^-?(?:0|[1-9]\d*)(?:\.\d+)?$"; + + schemaType |= JsonSchemaType.String; + } + + if (isIeeeFloatingPoint && (numberHandling & JsonNumberHandling.AllowNamedFloatingPointLiterals) != 0) + { + return new JsonSchema + { + AnyOf = + [ + new JsonSchema { Type = schemaType, Pattern = pattern }, + new JsonSchema { Enum = [(JsonNode)"NaN", (JsonNode)"Infinity", (JsonNode)"-Infinity"] }, + ] + }; + } + + return new JsonSchema { Type = schemaType, Pattern = pattern }; + } } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/MemoryByteConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/MemoryByteConverter.cs index 0efa7e71a13ac7..3d719375cba4bb 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/MemoryByteConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/MemoryByteConverter.cs @@ -1,6 +1,9 @@ // 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 System.Text.Json.Schema; + namespace System.Text.Json.Serialization.Converters { internal sealed class MemoryByteConverter : JsonConverter> @@ -16,5 +19,7 @@ public override void Write(Utf8JsonWriter writer, Memory value, JsonSerial { writer.WriteBase64StringValue(value.Span); } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => new() { Type = JsonSchemaType.String }; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ReadOnlyMemoryByteConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ReadOnlyMemoryByteConverter.cs index 48eddafefba200..5139026fc4cae5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ReadOnlyMemoryByteConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/ReadOnlyMemoryByteConverter.cs @@ -1,6 +1,9 @@ // 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 System.Text.Json.Schema; + namespace System.Text.Json.Serialization.Converters { internal sealed class ReadOnlyMemoryByteConverter : JsonConverter> @@ -16,5 +19,7 @@ public override void Write(Utf8JsonWriter writer, ReadOnlyMemory value, Js { writer.WriteBase64StringValue(value.Span); } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => new() { Type = JsonSchemaType.String }; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SByteConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SByteConverter.cs index f302fd6a04565a..0a9b91708d34da 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SByteConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SByteConverter.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -55,5 +57,8 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, sbyt writer.WriteNumberValue(value); } } + + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) => + GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs index 4c6b2c5a80674e..4d681a7b657094 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/SingleConverter.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -66,5 +68,8 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, floa writer.WriteNumberValue(value); } } + + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) => + GetSchemaForNumericType(JsonSchemaType.Number, numberHandling, isIeeeFloatingPoint: true); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/StringConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/StringConverter.cs index 7a2ce41367c634..8a290109e7e369 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/StringConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/StringConverter.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -50,5 +52,7 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, string val writer.WritePropertyName(value); } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => new() { Type = JsonSchemaType.String }; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeOnlyConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeOnlyConverter.cs index 29ee02876ac624..bd2c40c8a07cac 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeOnlyConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeOnlyConverter.cs @@ -3,6 +3,8 @@ using System.Buffers.Text; using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -93,5 +95,7 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, TimeOnly v writer.WritePropertyName(output.Slice(0, bytesWritten)); } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => new() { Type = JsonSchemaType.String, Format = "time" }; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeSpanConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeSpanConverter.cs index 08ef78d3f69732..e5a23f36d10003 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeSpanConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/TimeSpanConverter.cs @@ -3,6 +3,8 @@ using System.Buffers.Text; using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -91,5 +93,12 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, TimeSpan v writer.WritePropertyName(output.Slice(0, bytesWritten)); } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => new() + { + Type = JsonSchemaType.String, + Comment = "Represents a System.TimeSpan value.", + Pattern = @"^-?(\d+\.)?\d{2}:\d{2}:\d{2}(\.\d{1,7})?$" + }; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt128Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt128Converter.cs index d1dd1fc05fc6e5..711ba307045cb9 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt128Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt128Converter.cs @@ -4,6 +4,8 @@ using System.Buffers; using System.Diagnostics; using System.Globalization; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -127,6 +129,9 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, UInt } } + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) => + GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling); + // UInt128.TryParse(ROS) is not available on .NET 7, only UInt128.TryParse(ROS). private static bool TryParse( #if NET8_0_OR_GREATER diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt16Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt16Converter.cs index 16c717cb7d440d..f459f0434350d7 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt16Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt16Converter.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -57,5 +59,8 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, usho writer.WriteNumberValue((long)value); } } + + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) => + GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt32Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt32Converter.cs index 9a8dccb9b80cf6..b368e84638a543 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt32Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt32Converter.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -57,5 +59,8 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, uint writer.WriteNumberValue((ulong)value); } } + + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) => + GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt64Converter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt64Converter.cs index 7b3290a4787589..c42044f7e024ce 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt64Converter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UInt64Converter.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -55,5 +57,8 @@ internal override void WriteNumberWithCustomHandling(Utf8JsonWriter writer, ulon writer.WriteNumberValue(value); } } + + internal override JsonSchema? GetSchema(JsonNumberHandling numberHandling) => + GetSchemaForNumericType(JsonSchemaType.Integer, numberHandling); } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UnsupportedTypeConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UnsupportedTypeConverter.cs index f0d37569c8b338..f80af7c92b6c74 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UnsupportedTypeConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UnsupportedTypeConverter.cs @@ -1,6 +1,9 @@ // 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.Schema; +using System.Text.Json.Nodes; + namespace System.Text.Json.Serialization.Converters { internal sealed class UnsupportedTypeConverter : JsonConverter @@ -16,5 +19,8 @@ public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerial public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) => throw new NotSupportedException(ErrorMessage); + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => + new JsonSchema { Comment = "Unsupported .NET type", Not = JsonSchema.True }; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UriConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UriConverter.cs index 312ec7a6877a92..5c6bceef66bfc4 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UriConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/UriConverter.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -50,5 +52,8 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, Uri value, writer.WritePropertyName(value.OriginalString); } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => + new() { Type = JsonSchemaType.String, Format = "uri" }; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/VersionConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/VersionConverter.cs index 85e0d8f67398a4..fffa210475cd6e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/VersionConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Converters/Value/VersionConverter.cs @@ -2,6 +2,8 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; +using System.Text.Json.Nodes; +using System.Text.Json.Schema; namespace System.Text.Json.Serialization.Converters { @@ -123,5 +125,13 @@ internal override void WriteAsPropertyNameCore(Utf8JsonWriter writer, Version va writer.WritePropertyName(value.ToString()); #endif } + + internal override JsonSchema? GetSchema(JsonNumberHandling _) => + new() + { + Type = JsonSchemaType.String, + Comment = "Represents a version string.", + Pattern = @"^\d+(\.\d+){1,3}$", + }; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs index 8c61188630276e..fafa21ad0485d5 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonConverter.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; +using System.Text.Json.Schema; using System.Text.Json.Serialization.Converters; using System.Text.Json.Serialization.Metadata; @@ -200,6 +201,10 @@ internal static bool ShouldFlush(ref WriteStack state, Utf8JsonWriter writer) internal abstract void WriteAsPropertyNameCoreAsObject(Utf8JsonWriter writer, object? value, JsonSerializerOptions options, bool isWritingExtensionDataProperty); internal abstract void WriteNumberWithCustomHandlingAsObject(Utf8JsonWriter writer, object? value, JsonNumberHandling handling); + /// + /// Gets a schema from the type being converted + /// + internal virtual JsonSchema? GetSchema(JsonNumberHandling numberHandling) => null; // Whether a type (ConverterStrategy.Object) is deserialized using a parameterized constructor. internal virtual bool ConstructorIsParameterized { get; } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs index 4db70c6694e225..15702db934c73c 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonParameterInfo.cs @@ -123,9 +123,6 @@ public ICustomAttributeProvider? AttributeProvider internal JsonNumberHandling? NumberHandling => MatchingProperty.EffectiveNumberHandling; internal JsonTypeInfo JsonTypeInfo => MatchingProperty.JsonTypeInfo; internal bool ShouldDeserialize => !MatchingProperty.IsIgnored; - internal bool IsRequiredParameter => - Options.RespectRequiredConstructorParameters && - !HasDefaultValue && - !IsMemberInitializer; + internal bool IsRequiredParameter => !HasDefaultValue && !IsMemberInitializer; } } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs index cb7975b38c99c9..73f6633ff7b634 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfo.cs @@ -448,7 +448,9 @@ internal void Configure() if (IsRequired) { - if (!CanDeserialize && AssociatedParameter?.IsRequiredParameter != true) + if (!CanDeserialize && + !(AssociatedParameter?.IsRequiredParameter is true && + Options.RespectRequiredConstructorParameters)) { ThrowHelper.ThrowInvalidOperationException_JsonPropertyRequiredAndNotDeserializable(this); } diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs index a0219eafa1622a..1c62158aa85b71 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonPropertyInfoOfT.cs @@ -120,8 +120,12 @@ internal override void AddJsonParameterInfo(JsonParameterInfoValues parameterInf AssociatedParameter = new JsonParameterInfo(parameterInfoValues, this); // Overwrite the nullability annotation of property setter with the parameter. _isSetNullable = parameterInfoValues.IsNullable; - // If the property has been associated with a non-optional parameter, mark it as required. - _isRequired |= AssociatedParameter.IsRequiredParameter; + + if (Options.RespectRequiredConstructorParameters) + { + // If the property has been associated with a non-optional parameter, mark it as required. + _isRequired |= AssociatedParameter.IsRequiredParameter; + } } internal new JsonConverter EffectiveConverter diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs index 41431ee40e3e11..147de20f3c5a6e 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/Metadata/JsonTypeInfo.cs @@ -360,6 +360,7 @@ public JsonPolymorphismOptions? PolymorphismOptions internal bool PropertyMetadataSerializationNotSupported { get; set; } internal bool IsNullable => Converter.NullableElementConverter is not null; + internal bool CanBeNull => PropertyInfoForTypeInfo.PropertyTypeCanBeNull; [MethodImpl(MethodImplOptions.AggressiveInlining)] internal void ValidateCanBeUsedForPropertyMetadataSerialization() diff --git a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs index 5b16af8de3eeca..b5e2f99947e329 100644 --- a/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs +++ b/src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs @@ -936,5 +936,17 @@ public static void ThrowInvalidOperationException_PipeWriterDoesNotImplementUnfl { throw new InvalidOperationException(SR.Format(SR.PipeWriter_DoesNotImplementUnflushedBytes, pipeWriter.GetType().Name)); } + + [DoesNotReturn] + public static void ThrowNotSupportedException_JsonSchemaExporterDoesNotSupportReferenceHandlerPreserve() + { + throw new NotSupportedException(SR.JsonSchemaExporter_ReferenceHandlerPreserve_NotSupported); + } + + [DoesNotReturn] + public static void ThrowInvalidOperationException_JsonSchemaExporterDepthTooLarge() + { + throw new InvalidOperationException(SR.JsonSchemaExporter_DepthTooLarge); + } } } diff --git a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs new file mode 100644 index 00000000000000..13de2b5a0f85af --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.TestTypes.cs @@ -0,0 +1,1477 @@ +// 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; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reflection; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using System.Text.Json.Serialization.Tests; + +namespace System.Text.Json.Schema.Tests +{ + public abstract partial class JsonSchemaExporterTests : SerializerTests + { + public static IEnumerable GetTestData() => + from testCase in GetTestDataCore() + select new object[] { testCase }; + + public static IEnumerable GetTestDataUsingAllValues() => + from testCase in GetTestDataCore() + from expandedTestCase in testCase.GetTestDataForAllValues() + select new object[] { expandedTestCase }; + + public static IEnumerable GetTestDataCore() + { + // Primitives and built-in types + yield return new TestData( + Value: new(), + AdditionalValues: [null, 42, false, 3.14, 3.14M, new int[] { 1, 2, 3 }, new SimpleRecord(1, "str", false, 3.14)], + ExpectedJsonSchema: "true"); + + yield return new TestData(true, ExpectedJsonSchema: """{"type":"boolean"}"""); + yield return new TestData(42, ExpectedJsonSchema: """{"type":"integer"}"""); + yield return new TestData(42, ExpectedJsonSchema: """{"type":"integer"}"""); + yield return new TestData(42, ExpectedJsonSchema: """{"type":"integer"}"""); + yield return new TestData(42, ExpectedJsonSchema: """{"type":"integer"}"""); + yield return new TestData(42, ExpectedJsonSchema: """{"type":"integer"}"""); + yield return new TestData(42, ExpectedJsonSchema: """{"type":"integer"}"""); + yield return new TestData(42, ExpectedJsonSchema: """{"type":"integer"}"""); + yield return new TestData(42, ExpectedJsonSchema: """{"type":"integer"}"""); + yield return new TestData(1.2f, ExpectedJsonSchema: """{"type":"number"}"""); + yield return new TestData(3.14159d, ExpectedJsonSchema: """{"type":"number"}"""); + yield return new TestData(3.14159M, ExpectedJsonSchema: """{"type":"number"}"""); +#if NET + yield return new TestData(42, ExpectedJsonSchema: """{"type":"integer"}"""); + yield return new TestData(42, ExpectedJsonSchema: """{"type":"integer"}"""); + yield return new TestData((Half)3.141, ExpectedJsonSchema: """{"type":"number"}"""); +#endif + yield return new TestData("I am a string", ExpectedJsonSchema: """{"type":["string","null"]}"""); + yield return new TestData('c', ExpectedJsonSchema: """{"type":"string", "minLength":1, "maxLength":1 }"""); + yield return new TestData( + Value: [1, 2, 3], + AdditionalValues: [[]], + ExpectedJsonSchema: """{"type":["string","null"]}"""); + + yield return new TestData>(new byte[] { 1, 2, 3 }, ExpectedJsonSchema: """{"type":"string"}"""); + yield return new TestData>(new byte[] { 1, 2, 3 }, ExpectedJsonSchema: """{"type":"string"}"""); + yield return new TestData( + Value: new(2024, 06, 06, 21, 39, 42, DateTimeKind.Utc), + ExpectedJsonSchema: """{"type":"string","format":"date-time"}"""); + + yield return new TestData( + Value: new(new DateTime(2021, 1, 1), TimeSpan.Zero), + AdditionalValues: [DateTimeOffset.MinValue, DateTimeOffset.MaxValue], + ExpectedJsonSchema: """{"type":"string","format": "date-time"}"""); + + yield return new TestData( + Value: new(hours: 5, minutes: 13, seconds: 3), + AdditionalValues: [TimeSpan.MinValue, TimeSpan.MaxValue], + ExpectedJsonSchema: """{"$comment": "Represents a System.TimeSpan value.", "type":"string", "pattern": "^-?(\\d+\\.)?\\d{2}:\\d{2}:\\d{2}(\\.\\d{1,7})?$"}"""); + +#if NET + yield return new TestData(new(2021, 1, 1), ExpectedJsonSchema: """{"type":"string","format": "date"}"""); + yield return new TestData(new(hour: 22, minute: 30, second: 33, millisecond: 100), ExpectedJsonSchema: """{"type":"string","format": "time"}"""); +#endif + yield return new TestData(Guid.Empty, ExpectedJsonSchema: """{"type":"string","format":"uuid"}"""); + yield return new TestData(new("http://example.com"), """{"type":["string","null"],"format":"uri"}"""); + yield return new TestData(new(1, 2, 3, 4), ExpectedJsonSchema: """{"$comment": "Represents a version string.", "type":["string","null"],"pattern":"^\\d+(\\.\\d+){1,3}$"}"""); + yield return new TestData(JsonDocument.Parse("""[{ "x" : 42 }]"""), ExpectedJsonSchema: "true"); + yield return new TestData(JsonDocument.Parse("""[{ "x" : 42 }]""").RootElement, ExpectedJsonSchema: "true"); + yield return new TestData(JsonNode.Parse("""[{ "x" : 42 }]"""), ExpectedJsonSchema: "true"); + yield return new TestData((JsonValue)42, ExpectedJsonSchema: "true"); + yield return new TestData(new() { ["x"] = 42 }, ExpectedJsonSchema: """{"type":["object","null"]}"""); + yield return new TestData([(JsonNode)1, (JsonNode)2, (JsonNode)3], ExpectedJsonSchema: """{"type":["array","null"]}"""); + + // Enum types + yield return new TestData(IntEnum.A, ExpectedJsonSchema: """{"type":"integer"}"""); + yield return new TestData(StringEnum.A, ExpectedJsonSchema: """{"enum":["A","B","C"]}"""); + yield return new TestData(FlagsStringEnum.A, ExpectedJsonSchema: """{"type":"string"}"""); + + // Nullable types + yield return new TestData(true, AdditionalValues: [null], ExpectedJsonSchema: """{"type":["boolean","null"]}"""); + yield return new TestData(42, AdditionalValues: [null], ExpectedJsonSchema: """{"type":["integer","null"]}"""); + yield return new TestData(3.14, AdditionalValues: [null], ExpectedJsonSchema: """{"type":["number","null"]}"""); + yield return new TestData(Guid.Empty, AdditionalValues: [null], ExpectedJsonSchema: """{"type":["string","null"],"format":"uuid"}"""); + yield return new TestData(JsonDocument.Parse("{}").RootElement, AdditionalValues: [null], ExpectedJsonSchema: "true"); + yield return new TestData(IntEnum.A, AdditionalValues: [null], ExpectedJsonSchema: """{"type":["integer","null"]}"""); + yield return new TestData(StringEnum.A, AdditionalValues: [null], ExpectedJsonSchema: """{"enum":["A","B","C",null]}"""); + yield return new TestData( + new(1, "two", true, 3.14), + AdditionalValues: [null], + ExpectedJsonSchema: """ + { + "type":["object","null"], + "properties": { + "X": {"type":"integer"}, + "Y": {"type":"string"}, + "Z": {"type":"boolean"}, + "W": {"type":"number"} + } + } + """); + + // User-defined POCOs + yield return new TestData( + Value: new() { String = "string", StringNullable = "string", Int = 42, Double = 3.14, Boolean = true }, + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "String": { "type": "string" }, + "StringNullable": { "type": ["string", "null"] }, + "Int": { "type": "integer" }, + "Double": { "type": "number" }, + "Boolean": { "type": "boolean" } + } + } + """); + + yield return new TestData( + Value: new(1, "two", true, 3.14), + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "X": { "type": "integer" }, + "Y": { "type": "string" }, + "Z": { "type": "boolean" }, + "W": { "type": "number" } + }, + "required": ["X","Y","Z","W"] + } + """); + + yield return new TestData( + Value: new(1, "two", true, 3.14), + ExpectedJsonSchema: """ + { + "type": "object", + "properties": { + "X": { "type": "integer" }, + "Y": { "type": "string" }, + "Z": { "type": "boolean" }, + "W": { "type": "number" } + } + } + """); + + yield return new TestData( + Value: new(1, "two", true, 3.14, StringEnum.A), + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "X1": { "type": "integer" }, + "X2": { "type": "string" }, + "X3": { "type": "boolean" }, + "X4": { "type": "number" }, + "X5": { "enum": ["A", "B", "C"] }, + "Y1": { "type": "integer", "default": 42 }, + "Y2": { "type": "string", "default": "str" }, + "Y3": { "type": "boolean", "default": true }, + "Y4": { "type": "number", "default": 0 }, + "Y5": { "enum": ["A", "B", "C"], "default": "A" } + }, + "required": ["X1", "X2", "X3", "X4", "X5"] + } + """); + + yield return new TestData( + new() { X = "str1", Y = "str2", Z = 42 }, + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "Y": { "type": "string" }, + "Z": { "type": "integer" }, + "X": { "type": "string" } + }, + "required": [ "Y", "Z", "X" ] + } + """); + + yield return new TestData(new() { X = 1, Y = 2 }, ExpectedJsonSchema: """{"type":["object","null"],"properties":{"X":{"type":"integer"}}}"""); + yield return new TestData( + Value: new() { IntegerProperty = 1, StringProperty = "str" }, + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "int": { "type": "integer" }, + "str": { "type": ["string", "null"] } + } + } + """); + + yield return new TestData( + Value: new() { X = 1 }, + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { "X": { "type": ["string","integer"], "pattern": "^-?(?:0|[1-9]\\d*)$" } } + } + """); + + yield return new TestData( + Value: new() { + IntegerReadingFromString = 3, + DoubleReadingFromString = 3.14, + DecimalReadingFromString = 3.14M, + IntegerWritingAsString = 3, + DoubleWritingAsString = 3.14, + DecimalWritingAsString = 3.14M, + IntegerAllowingFloatingPointLiterals = 3, + DoubleAllowingFloatingPointLiterals = 3.14, + DecimalAllowingFloatingPointLiterals = 3.14M, + IntegerAllowingFloatingPointLiteralsAndReadingFromString = 3, + DoubleAllowingFloatingPointLiteralsAndReadingFromString = 3.14, + DecimalAllowingFloatingPointLiteralsAndReadingFromString = 3.14M, + }, + AdditionalValues: [ + new() { DoubleAllowingFloatingPointLiterals = double.NaN, }, + new() { DoubleAllowingFloatingPointLiterals = double.PositiveInfinity }, + new() { DoubleAllowingFloatingPointLiterals = double.NegativeInfinity }, + ], + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "IntegerReadingFromString": { "type": ["string","integer"], "pattern": "^-?(?:0|[1-9]\\d*)$" }, + "DoubleReadingFromString": { "type": ["string","number"], "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$" }, + "DecimalReadingFromString": { "type": ["string","number"], "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$" }, + "IntegerWritingAsString": { "type": ["string","integer"], "pattern": "^-?(?:0|[1-9]\\d*)$" }, + "DoubleWritingAsString": { "type": ["string","number"], "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$" }, + "DecimalWritingAsString": { "type": ["string","number"], "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$" }, + "IntegerAllowingFloatingPointLiterals": { "type": "integer" }, + "DoubleAllowingFloatingPointLiterals": { + "anyOf": [ + { "type": "number" }, + { "enum": ["NaN", "Infinity", "-Infinity"] } + ] + }, + "DecimalAllowingFloatingPointLiterals": { "type": "number" }, + "IntegerAllowingFloatingPointLiteralsAndReadingFromString": { "type": ["string","integer"], "pattern": "^-?(?:0|[1-9]\\d*)$" }, + "DoubleAllowingFloatingPointLiteralsAndReadingFromString": { + "anyOf": [ + { "type": ["string","number"], "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?$" }, + { "enum": ["NaN", "Infinity", "-Infinity"] } + ] + }, + "DecimalAllowingFloatingPointLiteralsAndReadingFromString": { "type": ["string","number"], "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$" } + } + } + """); + + yield return new TestData( + Value: new() { Value = 1, Next = new() { Value = 2, Next = new() { Value = 3 } } }, + AdditionalValues: [new() { Value = 1, Next = null }], + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "Value": { "type": "integer" }, + "Next": { + "type": ["object", "null"], + "properties": { + "Value": { "type": "integer" }, + "Next": { "$ref": "#/properties/Next" } + } + } + } + } + """); + + // Same as above with non-nullable reference type handling + yield return new TestData( + Value: new() { Value = 1, Next = new() { Value = 2, Next = new() { Value = 3 } } }, + AdditionalValues: [new() { Value = 1, Next = null }], + ExpectedJsonSchema: """ + { + "type": "object", + "properties": { + "Value": { "type": "integer" }, + "Next": { + "type": ["object", "null"], + "properties": { + "Value": { "type": "integer" }, + "Next": { "$ref": "#/properties/Next" } + } + } + } + } + """, + Options: new() { TreatNullObliviousAsNonNullable = true }); + + // Same as above but using an anchor-based reference scheme + yield return new TestData( + Value: new() { Value = 1, Next = new() { Value = 2, Next = new() { Value = 3 } } }, + AdditionalValues: [new() { Value = 1, Next = null }], + ExpectedJsonSchema: """ + { + "$anchor" : "PocoWithRecursiveMembers", + "type": ["object","null"], + "properties": { + "Value": { "type": "integer" }, + "Next": { + "$anchor" : "PocoWithRecursiveMembers_Next", + "type": ["object", "null"], + "properties": { + "Value": { "type": "integer" }, + "Next": { "$ref": "#PocoWithRecursiveMembers_Next" } + } + } + } + } + """, + Options: new JsonSchemaExporterOptions + { + TransformSchemaNode = static (ctx, schema) => + { + if (ctx.TypeInfo.Kind is JsonTypeInfoKind.None || schema is not JsonObject schemaObj) + { + return schema; + } + + string anchorName = ctx.PropertyInfo is { } property + ? ctx.TypeInfo.Type.Name + "_" + property.Name + : ctx.TypeInfo.Type.Name; + + if (schemaObj.ContainsKey("$ref")) + { + schemaObj["$ref"] = "#" + anchorName; + } + else + { + schemaObj["$anchor"] = anchorName; + } + + return schemaObj; + } + }); + + // Same as above but using an id-based reference scheme + yield return new TestData( + Value: new() { Value = 1, Next = new() { Value = 2, Next = new() { Value = 3 } } }, + AdditionalValues: [new() { Value = 1, Next = null }], + ExpectedJsonSchema: """ + { + "$id" : "https://example.com/schema/pocowithrecursivemembers.json", + "type": ["object","null"], + "properties": { + "Value": { "type": "integer" }, + "Next": { + "$id" : "https://example.com/schema/pocowithrecursivemembers/next.json", + "type": ["object", "null"], + "properties": { + "Value": { "type": "integer" }, + "Next": { "$ref": "https://example.com/schema/pocowithrecursivemembers/next.json" } + } + } + } + } + """, + Options: new JsonSchemaExporterOptions + { + TransformSchemaNode = static (ctx, schema) => + { + if (ctx.TypeInfo.Kind is JsonTypeInfoKind.None || schema is not JsonObject schemaObj) + { + return schema; + } + + string idPath = ctx.PropertyInfo is { } property + ? ctx.TypeInfo.Type.Name.ToLower() + "/" + property.Name.ToLower() + : ctx.TypeInfo.Type.Name.ToLower(); + + string idUrl = "https://example.com/schema/" + idPath + ".json"; + + if (schemaObj.ContainsKey("$ref")) + { + schemaObj["$ref"] = idUrl; + } + else + { + schemaObj["$id"] = idUrl; + } + + return schemaObj; + } + }); + + yield return new TestData( + Value: new() { Children = [new(), new() { Children = [] }] }, + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "Children": { + "type": "array", + "items": { "$ref" : "#" } + } + } + } + """); + + // Same as above but with non-nullable reference type handling + yield return new TestData( + Value: new() { Children = [new(), new() { Children = [] }] }, + ExpectedJsonSchema: """ + { + "type": "object", + "properties": { + "Children": { + "type": "array", + "items": { "$ref" : "#" } + } + } + } + """, + Options: new() { TreatNullObliviousAsNonNullable = true }); + + yield return new TestData( + Value: new() { Children = new() { ["key1"] = new(), ["key2"] = new() { Children = new() { ["key3"] = new() } } } }, + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "Children": { + "type": "object", + "additionalProperties": { "$ref" : "#" } + } + } + } + """); + + // Same as above but with non-nullable reference type handling + yield return new TestData( + Value: new() { Children = new() { ["key1"] = new(), ["key2"] = new() { Children = new() { ["key3"] = new() } } } }, + ExpectedJsonSchema: """ + { + "type": "object", + "properties": { + "Children": { + "type": "object", + "additionalProperties": { "$ref" : "#" } + } + } + } + """, + Options: new() { TreatNullObliviousAsNonNullable = true }); + + yield return new TestData( + Value: new() { X = 42 }, + ExpectedJsonSchema: """ + { + "description": "The type description", + "type": ["object","null"], + "properties": { + "X": { + "description": "The property description", + "type": "integer" + } + } + } + """, + Options: new() + { + TransformSchemaNode = static (ctx, schema) => + { + if (schema is not JsonObject schemaObj || schemaObj.ContainsKey("$ref")) + { + return schema; + } + + DescriptionAttribute? descriptionAttribute = + GetCustomAttribute(ctx.PropertyInfo?.AttributeProvider) ?? + GetCustomAttribute(ctx.PropertyInfo?.AssociatedParameter.AttributeProvider) ?? + GetCustomAttribute(ctx.TypeInfo.Type); + + if (descriptionAttribute != null) + { + schemaObj["description"] = (JsonNode)descriptionAttribute.Description; + } + + return schemaObj; + } + }); + + yield return new TestData(new() { Value = 42 }, ExpectedJsonSchema: "true"); + yield return new TestData(new() { Value = 42 }, ExpectedJsonSchema: """{"type":["object", "null"],"properties":{"Value":true}}"""); + yield return new TestData( + Value: new() + { + IntEnum = IntEnum.A, + StringEnum = StringEnum.B, + IntEnumUsingStringConverter = IntEnum.A, + NullableIntEnumUsingStringConverter = IntEnum.B, + StringEnumUsingIntConverter = StringEnum.A, + NullableStringEnumUsingIntConverter = StringEnum.B + }, + AdditionalValues: [ + new() + { + IntEnum = (IntEnum)int.MaxValue, + StringEnum = StringEnum.A, + IntEnumUsingStringConverter = IntEnum.A, + NullableIntEnumUsingStringConverter = null, + StringEnumUsingIntConverter = (StringEnum)int.MaxValue, + NullableStringEnumUsingIntConverter = null + }, + ], + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "IntEnum": { "type": "integer" }, + "StringEnum": { "enum": [ "A", "B", "C" ] }, + "IntEnumUsingStringConverter": { "enum": [ "A", "B", "C" ] }, + "NullableIntEnumUsingStringConverter": { "enum": [ "A", "B", "C", null ] }, + "StringEnumUsingIntConverter": { "type": "integer" }, + "NullableStringEnumUsingIntConverter": { "type": [ "integer", "null" ] } + } + } + """); + + // Same but using a callback to insert a type keyword for string enums. + yield return new TestData( + Value: new() + { + IntEnum = IntEnum.A, + StringEnum = StringEnum.B, + IntEnumUsingStringConverter = IntEnum.A, + NullableIntEnumUsingStringConverter = IntEnum.B, + StringEnumUsingIntConverter = StringEnum.A, + NullableStringEnumUsingIntConverter = StringEnum.B + }, + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "IntEnum": { "type": "integer" }, + "StringEnum": { "type" : "string", "enum": [ "A", "B", "C" ] }, + "IntEnumUsingStringConverter": { "type" : "string", "enum": [ "A", "B", "C" ] }, + "NullableIntEnumUsingStringConverter": { "type" : ["string","null"], "enum": [ "A", "B", "C", null ] }, + "StringEnumUsingIntConverter": { "type": "integer" }, + "NullableStringEnumUsingIntConverter": { "type": [ "integer", "null" ] } + } + } + """, + Options: new() + { + TransformSchemaNode = static (ctx, schema) => + { + if (schema is not JsonObject schemaObj) + { + return schema; + } + + Type type = ctx.TypeInfo.Type; + + if (schemaObj.ContainsKey("enum")) + { + if (ctx.TypeInfo.Type.IsEnum) + { + schemaObj.Add("type", "string"); + } + else if (Nullable.GetUnderlyingType(type)?.IsEnum is true) + { + schemaObj.Add("type", new JsonArray { (JsonNode)"string", (JsonNode)"null" }); + } + } + + return schemaObj; + } + }); + + var recordStruct = new SimpleRecordStruct(42, "str", true, 3.14); + yield return new TestData( + Value: new() { Struct = recordStruct, NullableStruct = null }, + AdditionalValues: [new() { Struct = recordStruct, NullableStruct = recordStruct }], + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "Struct": { + "type": "object", + "properties": { + "X": {"type":"integer"}, + "Y": {"type":"string"}, + "Z": {"type":"boolean"}, + "W": {"type":"number"} + } + }, + "NullableStruct": { + "type": ["object","null"], + "properties": { + "X": {"type":"integer"}, + "Y": {"type":"string"}, + "Z": {"type":"boolean"}, + "W": {"type":"number"} + } + } + } + } + """); + + yield return new TestData( + Value: new() { NullableStruct = null, Struct = recordStruct }, + AdditionalValues: [new() { NullableStruct = recordStruct, Struct = recordStruct }], + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "NullableStruct": { + "type": ["object","null"], + "properties": { + "X": {"type":"integer"}, + "Y": {"type":"string"}, + "Z": {"type":"boolean"}, + "W": {"type":"number"} + } + }, + "Struct": { + "type": "object", + "properties": { + "X": {"type":"integer"}, + "Y": {"type":"string"}, + "Z": {"type":"boolean"}, + "W": {"type":"number"} + } + } + } + } + """); + + yield return new TestData( + Value: new() { Name = "name", ExtensionData = new() { ["x"] = 42 } }, + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "Name": { "type": ["string", "null"] } + } + } + """); + + yield return new TestData( + Value: new() { Name = "name", Age = 42 }, + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "Name": {"type": ["string", "null"]}, + "Age": {"type":"integer"} + }, + "additionalProperties": false + } + """); + + yield return new TestData( + Value: new() { MaybeNull = null!, AllowNull = null, NotNull = null, DisallowNull = null!, NotNullDisallowNull = "str" }, + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "MaybeNull": {"type":["string","null"]}, + "AllowNull": {"type":["string","null"]}, + "NotNull": {"type":["string","null"]}, + "DisallowNull": {"type":["string","null"]}, + "NotNullDisallowNull": {"type":"string"} + } + } + """); + + yield return new TestData( + Value: new(allowNull: null, disallowNull: "str"), + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "AllowNull": {"type":["string","null"]}, + "DisallowNull": {"type":"string"} + }, + "required": ["AllowNull", "DisallowNull"] + } + """); + + yield return new TestData( + Value: new(null), + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "Value": {"type":["string","null"]} + }, + "required": ["Value"] + } + """); + + yield return new TestData( + Value: new(), + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "X1": {"type":"string", "default": "str" }, + "X2": {"type":"integer", "default": 42 }, + "X3": {"type":"boolean", "default": true }, + "X4": {"type":"number", "default": 0 }, + "X5": {"enum":["A","B","C"], "default": "A" }, + "X6": {"type":["string","null"], "default": "str" }, + "X7": {"type":["integer","null"], "default": 42 }, + "X8": {"type":["boolean","null"], "default": true }, + "X9": {"type":["number","null"], "default": 0 }, + "X10": {"enum":["A","B","C", null], "default": "A" } + } + } + """); + + yield return new TestData>( + Value: new(null!), + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "Value": {"type":["string","null"]} + }, + "required": ["Value"] + } + """); + + yield return new TestData( + Value: new PocoWithPolymorphism.DerivedPocoStringDiscriminator { BaseValue = 42, DerivedValue = "derived" }, + AdditionalValues: [ + new PocoWithPolymorphism.DerivedPocoNoDiscriminator { BaseValue = 42, DerivedValue = "derived" }, + new PocoWithPolymorphism.DerivedPocoIntDiscriminator { BaseValue = 42, DerivedValue = "derived" }, + new PocoWithPolymorphism.DerivedCollectionNoDiscriminator { BaseValue = 42 }, + new PocoWithPolymorphism.DerivedCollection { BaseValue = 42 }, + new PocoWithPolymorphism.DerivedDictionaryNoDiscriminator { BaseValue = 42 }, + new PocoWithPolymorphism.DerivedDictionary { BaseValue = 42 }, + ], + + ExpectedJsonSchema: """ + { + "anyOf": [ + { + "type": ["object","null"], + "properties": { + "BaseValue": {"type":"integer"}, + "DerivedValue": {"type":["string", "null"]} + } + }, + { + "type": ["object","null"], + "properties": { + "$type": {"const":"derivedPoco"}, + "BaseValue": {"type":"integer"}, + "DerivedValue": {"type":["string", "null"]} + }, + "required": ["$type"] + }, + { + "type": ["object","null"], + "properties": { + "$type": {"const":42}, + "BaseValue": {"type":"integer"}, + "DerivedValue": {"type":["string", "null"]} + }, + "required": ["$type"] + }, + { + "type": ["array","null"], + "items": {"type":"integer"} + }, + { + "type": ["object","null"], + "properties": { + "$type": {"const":"derivedCollection"}, + "$values": { + "type": "array", + "items": {"type":"integer"} + } + }, + "required": ["$type"] + }, + { + "type": ["object","null"], + "additionalProperties":{"type": "integer"} + }, + { + "type": ["object","null"], + "properties": { + "$type": {"const":"derivedDictionary"} + }, + "additionalProperties":{"type": "integer"}, + "required": ["$type"] + } + ] + } + """); + + yield return new TestData( + Value: new NonAbstractClassWithSingleDerivedType(), + AdditionalValues: [new NonAbstractClassWithSingleDerivedType.Derived()], + ExpectedJsonSchema: """{"type":["object","null"]}"""); + + yield return new TestData( + Value: new DiscriminatedUnion.Left("value"), + AdditionalValues: [new DiscriminatedUnion.Right(42)], + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "required": ["case"], + "anyOf": [ + { + "properties": { + "case": {"const":"left"}, + "value": {"type":"string"} + }, + "required": ["value"] + }, + { + "properties": { + "case": {"const":"right"}, + "value": {"type":"integer"} + }, + "required": ["value"] + } + ] + } + """); + + yield return new TestData( + Value: new(), + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "PolymorphicValue": { + "anyOf": [ + { + "type": "object", + "properties": { + "BaseValue": {"type":"integer"}, + "DerivedValue": {"type":["string", "null"]} + } + }, + { + "type": "object", + "properties": { + "$type": {"const":"derivedPoco"}, + "BaseValue": {"type":"integer"}, + "DerivedValue": {"type":["string","null"]} + }, + "required": ["$type"] + }, + { + "type": "object", + "properties": { + "$type": {"const":42}, + "BaseValue": {"type":"integer"}, + "DerivedValue": {"type":["string", "null"]} + }, + "required": ["$type"] + }, + { + "type": "array", + "items": {"type":"integer"} + }, + { + "type": "object", + "properties": { + "$type": {"const":"derivedCollection"}, + "$values": { + "type": "array", + "items": {"type":"integer"} + } + }, + "required": ["$type"] + }, + { + "type": "object", + "additionalProperties":{"type": "integer"} + }, + { + "type": "object", + "properties": { + "$type": {"const":"derivedDictionary"} + }, + "additionalProperties":{"type": "integer"}, + "required": ["$type"] + } + ] + }, + "DiscriminatedUnion":{ + "type": "object", + "required": ["case"], + "anyOf": [ + { + "properties": { + "case": {"const":"left"}, + "value": {"type":"string"} + }, + "required": ["value"] + }, + { + "properties": { + "case": {"const":"right"}, + "value": {"type":"integer"} + }, + "required": ["value"] + } + ] + }, + "DerivedValue1": { + "type": "object", + "properties": { + "BaseValue": {"type":"integer"}, + "DerivedValue": {"type":["string", "null"]} + } + }, + "DerivedValue2": { + "type": "object", + "properties": { + "BaseValue": {"type":"integer"}, + "DerivedValue": {"type":["string", "null"]} + } + } + } + } + """); + + yield return new TestData( + Value: new("string", -1), + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "StringValue": {"type":"string","pattern":"\\w+"}, + "IntValue": {"type":"integer","default":42} + }, + "required": ["StringValue","IntValue"] + } + """, + Options: new() + { + TransformSchemaNode = static (ctx, schema) => + { + if (ctx.PropertyInfo is null || schema is not JsonObject schemaObj || schemaObj.ContainsKey("$ref")) + { + return schema; + } + + DefaultValueAttribute? defaultValueAttr = + GetCustomAttribute(ctx.PropertyInfo?.AttributeProvider) ?? + GetCustomAttribute(ctx.PropertyInfo?.AssociatedParameter?.AttributeProvider); + + if (defaultValueAttr != null) + { + schemaObj["default"] = JsonSerializer.SerializeToNode(defaultValueAttr.Value, ctx.TypeInfo); + } + + RegularExpressionAttribute? regexAttr = + GetCustomAttribute(ctx.PropertyInfo?.AttributeProvider) ?? + GetCustomAttribute(ctx.PropertyInfo?.AssociatedParameter?.AttributeProvider); + + if (regexAttr != null) + { + schemaObj["pattern"] = regexAttr.Pattern; + } + + return schemaObj; + } + }); + + yield return new TestData( + Value: new ClassWithJsonPointerEscapablePropertyNames { Value = new() }, + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "properties": { + "~/path/to/value": { + "type": "object", + "properties": { + "Value" : {"type":"integer"}, + "Next": { + "type": ["object","null"], + "properties": { + "Value" : {"type":"integer"}, + "Next": {"$ref":"#/properties/~0~1path~1to~1value/properties/Next"} + } + } + } + } + } + } + """); + + // Collection types + yield return new TestData([1, 2, 3], ExpectedJsonSchema: """{"type":["array","null"],"items":{"type":"integer"}}"""); + yield return new TestData>([false, true, false], ExpectedJsonSchema: """{"type":["array","null"],"items":{"type":"boolean"}}"""); + yield return new TestData>(["one", "two", "three"], ExpectedJsonSchema: """{"type":["array","null"],"items":{"type":["string","null"]}}"""); + yield return new TestData>(new([1.1, 2.2, 3.3]), ExpectedJsonSchema: """{"type":["array","null"],"items":{"type":"number"}}"""); + yield return new TestData>(new(['x', '2', '+']), ExpectedJsonSchema: """{"type":["array","null"],"items":{"type":"string","minLength":1,"maxLength":1}}"""); + yield return new TestData>([1, 2, 3], ExpectedJsonSchema: """{"type":"array","items":{"type":"integer"}}"""); + yield return new TestData>(["one", "two", "three"], ExpectedJsonSchema: """{"type":["array","null"],"items":{"type":["string","null"]}}"""); + yield return new TestData>([false, false, true], ExpectedJsonSchema: """{"type":["array","null"],"items":{"type":"boolean"}}"""); + yield return new TestData([1, "two", 3.14], ExpectedJsonSchema: """{"type":["array","null"]}"""); + yield return new TestData([1, "two", 3.14], ExpectedJsonSchema: """{"type":["array","null"]}"""); + + // Dictionary types + yield return new TestData>( + Value: new() { ["one"] = 1, ["two"] = 2, ["three"] = 3 }, + ExpectedJsonSchema: """{"type":["object","null"],"additionalProperties":{"type": "integer"}}"""); + + yield return new TestData>( + Value: new([new("one", 1), new("two", 2), new("three", 3)]), + ExpectedJsonSchema: """{"type":"object","additionalProperties":{"type": "integer"}}"""); + + yield return new TestData>( + Value: new() { [1] = "one", [2] = "two", [3] = "three" }, + ExpectedJsonSchema: """{"type":["object","null"],"additionalProperties":{"type": ["string","null"]}}"""); + + yield return new TestData>( + Value: new() + { + ["one"] = new() { String = "string", StringNullable = "string", Int = 42, Double = 3.14, Boolean = true }, + ["two"] = new() { String = "string", StringNullable = null, Int = 42, Double = 3.14, Boolean = true }, + ["three"] = new() { String = "string", StringNullable = null, Int = 42, Double = 3.14, Boolean = true }, + }, + ExpectedJsonSchema: """ + { + "type": ["object","null"], + "additionalProperties": { + "type": ["object","null"], + "properties": { + "String": { "type": "string" }, + "StringNullable": { "type": ["string","null"] }, + "Int": { "type": "integer" }, + "Double": { "type": "number" }, + "Boolean": { "type": "boolean" } + } + } + } + """); + + yield return new TestData>( + Value: new() { ["one"] = 1, ["two"] = "two", ["three"] = 3.14 }, + ExpectedJsonSchema: """{"type":["object","null"]}"""); + + yield return new TestData( + Value: new() { ["one"] = 1, ["two"] = "two", ["three"] = 3.14 }, + ExpectedJsonSchema: """{"type":["object","null"]}"""); + } + + public enum IntEnum { A, B, C }; + + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum StringEnum { A, B, C }; + + [Flags, JsonConverter(typeof(JsonStringEnumConverter))] + public enum FlagsStringEnum { A = 1, B = 2, C = 4 }; + + public class SimplePoco + { + public string String { get; set; } = "default"; + public string? StringNullable { get; set; } + + public int Int { get; set; } + public double Double { get; set; } + public bool Boolean { get; set; } + } + + public record SimpleRecord(int X, string Y, bool Z, double W); + public record struct SimpleRecordStruct(int X, string Y, bool Z, double W); + + public record RecordWithOptionalParameters( + int X1, string X2, bool X3, double X4, StringEnum X5, + int Y1 = 42, string Y2 = "str", bool Y3 = true, double Y4 = 0, StringEnum Y5 = StringEnum.A); + + public class PocoWithRequiredMembers + { + [JsonInclude] + public required string X; + + public required string Y { get; set; } + + [JsonRequired] + public int Z { get; set; } + } + + public class PocoWithIgnoredMembers + { + public int X { get; set; } + + [JsonIgnore] + public int Y { get; set; } + } + + public class PocoWithCustomNaming + { + [JsonPropertyName("int")] + public int IntegerProperty { get; set; } + + [JsonPropertyName("str")] + public string? StringProperty { get; set; } + } + + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + public class PocoWithCustomNumberHandling + { + public int X { get; set; } + } + + public class PocoWithCustomNumberHandlingOnProperties + { + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + public int IntegerReadingFromString { get; set; } + + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + public double DoubleReadingFromString { get; set; } + + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + public decimal DecimalReadingFromString { get; set; } + + [JsonNumberHandling(JsonNumberHandling.WriteAsString)] + public int IntegerWritingAsString { get; set; } + + [JsonNumberHandling(JsonNumberHandling.WriteAsString)] + public double DoubleWritingAsString { get; set; } + + [JsonNumberHandling(JsonNumberHandling.WriteAsString)] + public decimal DecimalWritingAsString { get; set; } + + [JsonNumberHandling(JsonNumberHandling.AllowNamedFloatingPointLiterals)] + public int IntegerAllowingFloatingPointLiterals { get; set; } + + [JsonNumberHandling(JsonNumberHandling.AllowNamedFloatingPointLiterals)] + public double DoubleAllowingFloatingPointLiterals { get; set; } + + [JsonNumberHandling(JsonNumberHandling.AllowNamedFloatingPointLiterals)] + public decimal DecimalAllowingFloatingPointLiterals { get; set; } + + [JsonNumberHandling(JsonNumberHandling.AllowNamedFloatingPointLiterals | JsonNumberHandling.AllowReadingFromString)] + public int IntegerAllowingFloatingPointLiteralsAndReadingFromString { get; set; } + + [JsonNumberHandling(JsonNumberHandling.AllowNamedFloatingPointLiterals | JsonNumberHandling.AllowReadingFromString)] + public double DoubleAllowingFloatingPointLiteralsAndReadingFromString { get; set; } + + [JsonNumberHandling(JsonNumberHandling.AllowNamedFloatingPointLiterals | JsonNumberHandling.AllowReadingFromString)] + public decimal DecimalAllowingFloatingPointLiteralsAndReadingFromString { get; set; } + } + + public class PocoWithRecursiveMembers + { + public int Value { get; init; } + public PocoWithRecursiveMembers? Next { get; init; } + } + + public class PocoWithRecursiveCollectionElement + { + public List Children { get; init; } = new(); + } + + public class PocoWithRecursiveDictionaryValue + { + public Dictionary Children { get; init; } = new(); + } + + [Description("The type description")] + public class PocoWithDescription + { + [Description("The property description")] + public int X { get; set; } + } + + [JsonConverter(typeof(CustomConverter))] + public class PocoWithCustomConverter + { + public int Value { get; set; } + + public class CustomConverter : JsonConverter + { + public override PocoWithCustomConverter Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + new PocoWithCustomConverter { Value = reader.GetInt32() }; + + public override void Write(Utf8JsonWriter writer, PocoWithCustomConverter value, JsonSerializerOptions options) => + writer.WriteNumberValue(value.Value); + } + } + + public class PocoWithCustomPropertyConverter + { + [JsonConverter(typeof(CustomConverter))] + public int Value { get; set; } + + public class CustomConverter : JsonConverter + { + public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + => int.Parse(reader.GetString()!); + + public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) + => writer.WriteStringValue(value.ToString()); + } + } + + public class PocoWithEnums + { + public IntEnum IntEnum { get; init; } + public StringEnum StringEnum { get; init; } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public IntEnum IntEnumUsingStringConverter { get; set; } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public IntEnum? NullableIntEnumUsingStringConverter { get; set; } + + [JsonConverter(typeof(JsonNumberEnumConverter))] + public StringEnum StringEnumUsingIntConverter { get; set; } + + [JsonConverter(typeof(JsonNumberEnumConverter))] + public StringEnum? NullableStringEnumUsingIntConverter { get; set; } + } + + public class PocoWithStructFollowedByNullableStruct + { + public SimpleRecordStruct? NullableStruct { get; set; } + public SimpleRecordStruct Struct { get; set; } + } + + public class PocoWithNullableStructFollowedByStruct + { + public SimpleRecordStruct? NullableStruct { get; set; } + public SimpleRecordStruct Struct { get; set; } + } + + public class PocoWithExtensionDataProperty + { + public string? Name { get; set; } + + [JsonExtensionData] + public Dictionary? ExtensionData { get; set; } + } + + [JsonUnmappedMemberHandling(JsonUnmappedMemberHandling.Disallow)] + public class PocoDisallowingUnmappedMembers + { + public string? Name { get; set; } + public int Age { get; set; } + } + + public class PocoWithNullableAnnotationAttributes + { + [MaybeNull] + public string MaybeNull { get; set; } + + [AllowNull] + public string AllowNull { get; set; } + + [NotNull] + public string? NotNull { get; set; } + + [DisallowNull] + public string? DisallowNull { get; set; } + + [NotNull, DisallowNull] + public string? NotNullDisallowNull { get; set; } = ""; + } + + public class PocoWithNullableAnnotationAttributesOnConstructorParams([AllowNull] string allowNull, [DisallowNull] string? disallowNull) + { + public string AllowNull { get; } = allowNull!; + public string DisallowNull { get; } = disallowNull; + } + + public class PocoWithNullableConstructorParameter(string? value) + { + public string Value { get; } = value!; + } + + public class PocoWithOptionalConstructorParams( + string x1 = "str", int x2 = 42, bool x3 = true, double x4 = 0, StringEnum x5 = StringEnum.A, + string? x6 = "str", int? x7 = 42, bool? x8 = true, double? x9 = 0, StringEnum? x10 = StringEnum.A) + { + public string X1 { get; } = x1; + public int X2 { get; } = x2; + public bool X3 { get; } = x3; + public double X4 { get; } = x4; + public StringEnum X5 { get; } = x5; + + public string? X6 { get; } = x6; + public int? X7 { get; } = x7; + public bool? X8 { get; } = x8; + public double? X9 { get; } = x9; + public StringEnum? X10 { get; } = x10; + } + + // Regression test for https://github.com/dotnet/runtime/issues/92487 + public class GenericPocoWithNullableConstructorParameter(T value) + { + [NotNull] + public T Value { get; } = value!; + } + + [JsonDerivedType(typeof(DerivedPocoNoDiscriminator))] + [JsonDerivedType(typeof(DerivedPocoStringDiscriminator), "derivedPoco")] + [JsonDerivedType(typeof(DerivedPocoIntDiscriminator), 42)] + [JsonDerivedType(typeof(DerivedCollectionNoDiscriminator))] + [JsonDerivedType(typeof(DerivedCollection), "derivedCollection")] + [JsonDerivedType(typeof(DerivedDictionaryNoDiscriminator))] + [JsonDerivedType(typeof(DerivedDictionary), "derivedDictionary")] + public abstract class PocoWithPolymorphism + { + public int BaseValue { get; set; } + + public class DerivedPocoNoDiscriminator : PocoWithPolymorphism + { + public string? DerivedValue { get; set; } + } + + public class DerivedPocoStringDiscriminator : PocoWithPolymorphism + { + public string? DerivedValue { get; set; } + } + + public class DerivedPocoIntDiscriminator : PocoWithPolymorphism + { + public string? DerivedValue { get; set; } + } + + public class DerivedCollection : PocoWithPolymorphism, IEnumerable + { + public IEnumerator GetEnumerator() => Enumerable.Repeat(BaseValue, 1).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + public class DerivedCollectionNoDiscriminator : DerivedCollection; + + public class DerivedDictionary : PocoWithPolymorphism, IReadOnlyDictionary + { + public int this[string key] => key == nameof(BaseValue) ? BaseValue : throw new KeyNotFoundException(); + public IEnumerable Keys => [nameof(BaseValue)]; + public IEnumerable Values => [BaseValue]; + public int Count => 1; + public bool ContainsKey(string key) => key == nameof(BaseValue); + public bool TryGetValue(string key, out int value) => key == nameof(BaseValue) ? (value = BaseValue) == BaseValue : (value = 0) == 0; + public IEnumerator> GetEnumerator() => Enumerable.Repeat(new KeyValuePair(nameof(BaseValue), BaseValue), 1).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } + + public class DerivedDictionaryNoDiscriminator : DerivedDictionary; + } + + [JsonDerivedType(typeof(Derived))] + public class NonAbstractClassWithSingleDerivedType + { + public class Derived : NonAbstractClassWithSingleDerivedType; + } + + [JsonPolymorphic(TypeDiscriminatorPropertyName = "case")] + [JsonDerivedType(typeof(Left), "left")] + [JsonDerivedType(typeof(Right), "right")] + public abstract record DiscriminatedUnion + { + public record Left(string value) : DiscriminatedUnion; + public record Right(int value) : DiscriminatedUnion; + } + + public class PocoCombiningPolymorphicTypeAndDerivedTypes + { + public PocoWithPolymorphism PolymorphicValue { get; set; } = new PocoWithPolymorphism.DerivedPocoNoDiscriminator { DerivedValue = "derived" }; + public DiscriminatedUnion DiscriminatedUnion { get; set; } = new DiscriminatedUnion.Left("value"); + public PocoWithPolymorphism.DerivedPocoNoDiscriminator DerivedValue1 { get; set; } = new() { DerivedValue = "derived" }; + public PocoWithPolymorphism.DerivedPocoStringDiscriminator DerivedValue2 { get; set; } = new() { DerivedValue = "derived" }; + } + + public class ClassWithComponentModelAttributes + { + public ClassWithComponentModelAttributes(string stringValue, [DefaultValue(42)] int intValue) + { + StringValue = stringValue; + IntValue = intValue; + } + + [RegularExpression(@"\w+")] + public string StringValue { get; } + + public int IntValue { get; } + } + + public class ClassWithJsonPointerEscapablePropertyNames + { + [JsonPropertyName("~/path/to/value")] + public PocoWithRecursiveMembers Value { get; set; } + } + + public readonly struct StructDictionary(IEnumerable> values) + : IReadOnlyDictionary + where TKey : notnull + { + private readonly IReadOnlyDictionary _dictionary = values.ToDictionary(kvp => kvp.Key, kvp => kvp.Value); + public TValue this[TKey key] => _dictionary[key]; + public IEnumerable Keys => _dictionary.Keys; + public IEnumerable Values => _dictionary.Values; + public int Count => _dictionary.Count; + public bool ContainsKey(TKey key) => _dictionary.ContainsKey(key); + public IEnumerator> GetEnumerator() => _dictionary.GetEnumerator(); +#if NET + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => _dictionary.TryGetValue(key, out value); +#else + public bool TryGetValue(TKey key, out TValue value) => _dictionary.TryGetValue(key, out value); +#endif + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_dictionary).GetEnumerator(); + } + + public record TestData( + T? Value, + string ExpectedJsonSchema, + IEnumerable? AdditionalValues = null, + JsonSchemaExporterOptions? Options = null) + : ITestData + { + public Type Type => typeof(T); + object? ITestData.Value => Value; + + IEnumerable ITestData.GetTestDataForAllValues() + { + yield return this; + + if (default(T) is null && Options?.TreatNullObliviousAsNonNullable != true) + { + yield return this with { Value = default, AdditionalValues = null }; + } + + if (AdditionalValues != null) + { + foreach (T? value in AdditionalValues) + { + yield return this with { Value = value, AdditionalValues = null }; + } + } + } + } + + public interface ITestData + { + Type Type { get; } + + object? Value { get; } + + string ExpectedJsonSchema { get; } + + JsonSchemaExporterOptions? Options { get; } + + IEnumerable GetTestDataForAllValues(); + } + + private static TAttribute? GetCustomAttribute(ICustomAttributeProvider? provider, bool inherit = false) where TAttribute : Attribute + => provider?.GetCustomAttributes(typeof(TAttribute), inherit).FirstOrDefault() as TAttribute; + } +} diff --git a/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs new file mode 100644 index 00000000000000..042e78ad2bd6e8 --- /dev/null +++ b/src/libraries/System.Text.Json/tests/Common/JsonSchemaExporterTests.cs @@ -0,0 +1,209 @@ +// 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.Linq; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using System.Text.Json.Serialization.Metadata; +using System.Text.Json.Serialization.Tests; +using Json.Schema; +using Xunit; +using Xunit.Sdk; + +namespace System.Text.Json.Schema.Tests +{ + public abstract partial class JsonSchemaExporterTests : SerializerTests + { + private readonly JsonSerializerOptions _indentedOptions; + + protected JsonSchemaExporterTests(JsonSerializerWrapper serializer) : base(serializer) + { + _indentedOptions = new(serializer.DefaultOptions) { WriteIndented = true }; + } + + [Theory] + [MemberData(nameof(GetTestData))] + public void TestTypes_GeneratesExpectedJsonSchema(ITestData testData) + { + JsonNode schema = Serializer.DefaultOptions.GetJsonSchemaAsNode(testData.Type, testData.Options); + AssertValidJsonSchema(testData.Type, testData.ExpectedJsonSchema, schema); + } + + [Theory] + [MemberData(nameof(GetTestDataUsingAllValues))] + public void TestTypes_SerializedValueMatchesGeneratedSchema(ITestData testData) + { + JsonNode schema = Serializer.DefaultOptions.GetJsonSchemaAsNode(testData.Type, testData.Options); + JsonNode? instance = JsonSerializer.SerializeToNode(testData.Value, testData.Type, Serializer.DefaultOptions); + AssertDocumentMatchesSchema(schema, instance); + } + + [Theory] + [InlineData(typeof(string), "string")] + [InlineData(typeof(int[]), "array")] + [InlineData(typeof(Dictionary), "object")] + [InlineData(typeof(SimplePoco), "object")] + public void TreatNullObliviousAsNonNullable_False_MarksReferenceTypesAsNullable(Type referenceType, string expectedType) + { + Assert.True(!referenceType.IsValueType); + var config = new JsonSchemaExporterOptions { TreatNullObliviousAsNonNullable = false }; + JsonNode schema = Serializer.DefaultOptions.GetJsonSchemaAsNode(referenceType, config); + JsonArray arr = Assert.IsType(schema["type"]); + Assert.Equal([expectedType, "null"], arr.Select(e => (string)e!)); + } + + [Theory] + [InlineData(typeof(string), "string")] + [InlineData(typeof(int[]), "array")] + [InlineData(typeof(Dictionary), "object")] + [InlineData(typeof(SimplePoco), "object")] + public void TreatNullObliviousAsNonNullable_True_MarksReferenceTypesAsNonNullable(Type referenceType, string expectedType) + { + Assert.True(!referenceType.IsValueType); + var config = new JsonSchemaExporterOptions { TreatNullObliviousAsNonNullable = true }; + JsonNode schema = Serializer.DefaultOptions.GetJsonSchemaAsNode(referenceType, config); + Assert.Equal(expectedType, (string)schema["type"]!); + } + + [Theory] + [InlineData(typeof(Type))] + [InlineData(typeof(MethodInfo))] + [InlineData(typeof(UIntPtr))] + [InlineData(typeof(MemberInfo))] + [InlineData(typeof(SerializationInfo))] + [InlineData(typeof(Func))] + public void UnsupportedType_ReturnsExpectedSchema(Type type) + { + JsonNode schema = Serializer.DefaultOptions.GetJsonSchemaAsNode(type); + Assert.Equal(""""{"$comment":"Unsupported .NET type","not":true}"""", schema.ToJsonString()); + } + + [Fact] + public void TypeWithDisallowUnmappedMembers_AdditionalPropertiesFailValidation() + { + JsonNode schema = Serializer.DefaultOptions.GetJsonSchemaAsNode(typeof(PocoDisallowingUnmappedMembers)); + JsonNode? jsonWithUnmappedProperties = JsonNode.Parse("""{ "UnmappedProperty" : {} }"""); + AssertDoesNotMatchSchema(schema, jsonWithUnmappedProperties); + } + + [Fact] + public void GetJsonSchemaAsNode_NullInputs_ThrowsArgumentNullException() + { + Assert.Throws(() => ((JsonSerializerOptions)null!).GetJsonSchemaAsNode(typeof(int))); + Assert.Throws(() => Serializer.DefaultOptions.GetJsonSchemaAsNode((Type)null!)); + Assert.Throws(() => ((JsonTypeInfo)null!).GetJsonSchemaAsNode()); + } + + [Fact] + public void GetJsonSchemaAsNode_NoResolver_ThrowInvalidOperationException() + { + var options = new JsonSerializerOptions(); + Assert.Throws(() => options.GetJsonSchemaAsNode(typeof(int))); + } + + [Fact] + public void JsonSerializerOptions_SmallMaxDepth_ThrowsInvalidOperationException() + { + var options = new JsonSerializerOptions(Serializer.DefaultOptions) { MaxDepth = 1 }; + var ex = Assert.Throws(() => options.GetJsonSchemaAsNode(typeof(PocoWithRecursiveMembers))); + Assert.Contains("depth", ex.Message); + } + + [Fact] + public void ReferenceHandlePreserve_Enabled_ThrowsNotSupportedException() + { + var options = new JsonSerializerOptions(Serializer.DefaultOptions) { ReferenceHandler = ReferenceHandler.Preserve }; + options.MakeReadOnly(); + + var ex = Assert.Throws(() => options.GetJsonSchemaAsNode(typeof(SimplePoco))); + Assert.Contains("ReferenceHandler.Preserve", ex.Message); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void JsonSchemaExporterOptions_DefaultSettings(bool useSingleton) + { + JsonSchemaExporterOptions options = useSingleton ? JsonSchemaExporterOptions.Default : new(); + + Assert.False(options.TreatNullObliviousAsNonNullable); + Assert.Null(options.TransformSchemaNode); + } + + [Fact] + public void JsonSchemaExporterOptions_Default_IsSame() + { + Assert.Same(JsonSchemaExporterOptions.Default, JsonSchemaExporterOptions.Default); + } + + protected void AssertValidJsonSchema(Type type, string expectedJsonSchema, JsonNode actualJsonSchema) + { + JsonNode? expectedJsonSchemaNode = JsonNode.Parse(expectedJsonSchema, documentOptions: new() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }); + + if (!JsonNode.DeepEquals(expectedJsonSchemaNode, actualJsonSchema)) + { + throw new XunitException($""" + Generated schema does not match the expected specification. + Expected: + {FormatJson(expectedJsonSchemaNode)} + Actual: + {FormatJson(actualJsonSchema)} + """); + } + } + + protected void AssertDocumentMatchesSchema(JsonNode schema, JsonNode? instance) + { + EvaluationResults results = EvaluateSchemaCore(schema, instance); + if (!results.IsValid) + { + IEnumerable errors = results.Details + .Where(d => d.HasErrors) + .SelectMany(d => d.Errors!.Select(error => $"Path:${d.InstanceLocation} {error.Key}:{error.Value}")); + + throw new XunitException($""" + Instance JSON document does not match the specified schema. + Schema: + {FormatJson(schema)} + Instance: + {FormatJson(instance)} + Errors: + {string.Join(Environment.NewLine, errors)} + """); + } + } + + protected void AssertDoesNotMatchSchema(JsonNode schema, JsonNode? instance) + { + EvaluationResults results = EvaluateSchemaCore(schema, instance); + if (results.IsValid) + { + throw new XunitException($""" + Instance JSON document matches the specified schema. + Schema: + {FormatJson(schema)} + Instance: + {FormatJson(instance)} + """); + } + } + + private EvaluationResults EvaluateSchemaCore(JsonNode schema, JsonNode? instance) + { + JsonSchema jsonSchema = JsonSchema.FromText(schema.ToJsonString()); + return jsonSchema.Evaluate(instance, s_evaluationOptions); + } + + private static readonly EvaluationOptions s_evaluationOptions = new() + { + OutputFormat = OutputFormat.List, + RequireFormatValidation = true, + }; + + private string FormatJson(JsonNode? node) => + JsonSerializer.Serialize(node, _indentedOptions); + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs new file mode 100644 index 00000000000000..f188c0f241909f --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/Serialization/JsonSchemaExporterTests.cs @@ -0,0 +1,128 @@ +// 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; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Reflection; +using System.Runtime.Serialization; +using System.Text.Json.Nodes; +using System.Text.Json.Schema.Tests; +using System.Text.Json.Serialization; + +namespace System.Text.Json.SourceGeneration.Tests +{ + public sealed partial class JsonSchemaExporterTests_SourceGen() + : JsonSchemaExporterTests(new StringSerializerWrapper(TestTypesContext.Default)) + { + [JsonSerializable(typeof(object))] + [JsonSerializable(typeof(bool))] + [JsonSerializable(typeof(byte))] + [JsonSerializable(typeof(ushort))] + [JsonSerializable(typeof(uint))] + [JsonSerializable(typeof(ulong))] + [JsonSerializable(typeof(sbyte))] + [JsonSerializable(typeof(short))] + [JsonSerializable(typeof(int))] + [JsonSerializable(typeof(long))] + [JsonSerializable(typeof(float))] + [JsonSerializable(typeof(double))] + [JsonSerializable(typeof(decimal))] +#if NETCOREAPP + [JsonSerializable(typeof(UInt128))] + [JsonSerializable(typeof(Int128))] + [JsonSerializable(typeof(Half))] +#endif + [JsonSerializable(typeof(string))] + [JsonSerializable(typeof(char))] + [JsonSerializable(typeof(byte[]))] + [JsonSerializable(typeof(Memory))] + [JsonSerializable(typeof(ReadOnlyMemory))] + [JsonSerializable(typeof(DateTime))] + [JsonSerializable(typeof(DateTimeOffset))] + [JsonSerializable(typeof(TimeSpan))] +#if NETCOREAPP + [JsonSerializable(typeof(DateOnly))] + [JsonSerializable(typeof(TimeOnly))] +#endif + [JsonSerializable(typeof(Guid))] + [JsonSerializable(typeof(Uri))] + [JsonSerializable(typeof(Version))] + [JsonSerializable(typeof(JsonDocument))] + [JsonSerializable(typeof(JsonElement))] + [JsonSerializable(typeof(JsonNode))] + [JsonSerializable(typeof(JsonValue))] + [JsonSerializable(typeof(JsonObject))] + [JsonSerializable(typeof(JsonArray))] + // Unsupported types + [JsonSerializable(typeof(Type))] + [JsonSerializable(typeof(MethodInfo))] + [JsonSerializable(typeof(UIntPtr))] + [JsonSerializable(typeof(MemberInfo))] + [JsonSerializable(typeof(SerializationInfo))] + [JsonSerializable(typeof(Func))] + // Enum types + [JsonSerializable(typeof(IntEnum))] + [JsonSerializable(typeof(StringEnum))] + [JsonSerializable(typeof(FlagsStringEnum))] + // Nullable types + [JsonSerializable(typeof(bool?))] + [JsonSerializable(typeof(int?))] + [JsonSerializable(typeof(double?))] + [JsonSerializable(typeof(Guid?))] + [JsonSerializable(typeof(JsonElement?))] + [JsonSerializable(typeof(IntEnum?))] + [JsonSerializable(typeof(StringEnum?))] + [JsonSerializable(typeof(SimpleRecordStruct?))] + // User-defined POCOs + [JsonSerializable(typeof(SimplePoco))] + [JsonSerializable(typeof(SimpleRecord))] + [JsonSerializable(typeof(SimpleRecordStruct))] + [JsonSerializable(typeof(RecordWithOptionalParameters))] + [JsonSerializable(typeof(PocoWithRequiredMembers))] + [JsonSerializable(typeof(PocoWithIgnoredMembers))] + [JsonSerializable(typeof(PocoWithCustomNaming))] + [JsonSerializable(typeof(PocoWithCustomNumberHandling))] + [JsonSerializable(typeof(PocoWithCustomNumberHandlingOnProperties))] + [JsonSerializable(typeof(PocoWithRecursiveMembers))] + [JsonSerializable(typeof(PocoWithRecursiveCollectionElement))] + [JsonSerializable(typeof(PocoWithRecursiveDictionaryValue))] + [JsonSerializable(typeof(PocoWithDescription))] + [JsonSerializable(typeof(PocoWithCustomConverter))] + [JsonSerializable(typeof(PocoWithCustomPropertyConverter))] + [JsonSerializable(typeof(PocoWithEnums))] + [JsonSerializable(typeof(PocoWithStructFollowedByNullableStruct))] + [JsonSerializable(typeof(PocoWithNullableStructFollowedByStruct))] + [JsonSerializable(typeof(PocoWithExtensionDataProperty))] + [JsonSerializable(typeof(PocoDisallowingUnmappedMembers))] + [JsonSerializable(typeof(PocoWithNullableAnnotationAttributes))] + [JsonSerializable(typeof(PocoWithNullableAnnotationAttributesOnConstructorParams))] + [JsonSerializable(typeof(PocoWithNullableConstructorParameter))] + [JsonSerializable(typeof(PocoWithOptionalConstructorParams))] + [JsonSerializable(typeof(GenericPocoWithNullableConstructorParameter))] + [JsonSerializable(typeof(PocoWithPolymorphism))] + [JsonSerializable(typeof(DiscriminatedUnion))] + [JsonSerializable(typeof(NonAbstractClassWithSingleDerivedType))] + [JsonSerializable(typeof(PocoCombiningPolymorphicTypeAndDerivedTypes))] + [JsonSerializable(typeof(ClassWithComponentModelAttributes))] + [JsonSerializable(typeof(ClassWithJsonPointerEscapablePropertyNames))] + // Collection types + [JsonSerializable(typeof(int[]))] + [JsonSerializable(typeof(List))] + [JsonSerializable(typeof(HashSet))] + [JsonSerializable(typeof(Queue))] + [JsonSerializable(typeof(Stack))] + [JsonSerializable(typeof(ImmutableArray))] + [JsonSerializable(typeof(ImmutableList))] + [JsonSerializable(typeof(ImmutableQueue))] + [JsonSerializable(typeof(object[]))] + [JsonSerializable(typeof(System.Collections.ArrayList))] + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(SortedDictionary))] + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(Dictionary))] + [JsonSerializable(typeof(Hashtable))] + [JsonSerializable(typeof(StructDictionary))] + public partial class TestTypesContext : JsonSerializerContext; + } +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets index 7de497308e6bac..eede2cd52eeb0d 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.SourceGeneration.Tests/System.Text.Json.SourceGeneration.Tests.targets @@ -83,6 +83,8 @@ + + @@ -122,6 +124,7 @@ + @@ -144,6 +147,11 @@ + + + + + diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSchemaExporterTests.cs b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSchemaExporterTests.cs new file mode 100644 index 00000000000000..373c47b4fa604d --- /dev/null +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/Serialization/JsonSchemaExporterTests.cs @@ -0,0 +1,9 @@ +// 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.Serialization.Tests; + +namespace System.Text.Json.Schema.Tests +{ + public sealed class JsonSchemaExporterTests_Reflection() : JsonSchemaExporterTests(JsonSerializerWrapper.StringSerializer); +} diff --git a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj index 72a7a32210a31a..5a69d16fe40934 100644 --- a/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj +++ b/src/libraries/System.Text.Json/tests/System.Text.Json.Tests/System.Text.Json.Tests.csproj @@ -67,6 +67,8 @@ + + @@ -175,6 +177,7 @@ + @@ -311,9 +314,14 @@ + + + + +