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