JsonSchemaExporter
Functionality for exporting JSON schema from serialization contracts defined in JsonTypeInfo.
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
namespace System.Text.Json.Schema
{
[System.Runtime.CompilerServices.NullableContext(1)]
[System.Runtime.CompilerServices.Nullable(0)]
public static class JsonSchemaExporter
{
private readonly ref struct GenerationState
{
private readonly List<string> _currentPath;
private readonly Dictionary<(JsonTypeInfo, JsonPropertyInfo), string[]> _generated;
public int CurrentDepth => _currentPath.Count;
public JsonSerializerOptions Options { get; }
public JsonSchemaExporterOptions ExporterOptions { get; }
public GenerationState(JsonSerializerOptions options, JsonSchemaExporterOptions exporterOptions)
{
_currentPath = new List<string>();
_generated = new Dictionary<(JsonTypeInfo, JsonPropertyInfo), string[]>();
Options = options;
ExporterOptions = exporterOptions;
}
public void PushSchemaNode(string nodeId)
{
if (CurrentDepth == Options.EffectiveMaxDepth)
ThrowHelper.ThrowInvalidOperationException_JsonSchemaExporterDepthTooLarge();
_currentPath.Add(nodeId);
}
public void PopSchemaNode()
{
_currentPath.RemoveAt(_currentPath.Count - 1);
}
public bool TryGetExistingJsonPointer([In] [System.Runtime.CompilerServices.IsReadOnly] ref JsonSchemaExporterContext context, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out string existingJsonPointer)
{
(JsonTypeInfo, JsonPropertyInfo) key = (context.TypeInfo, context.PropertyInfo);
if (_generated.TryGetValue(key, out string[] value)) {
existingJsonPointer = FormatJsonPointer(value);
return true;
}
_generated[key] = context._path;
existingJsonPointer = null;
return false;
}
public JsonSchemaExporterContext CreateContext(JsonTypeInfo typeInfo, JsonPropertyInfo propertyInfo, JsonTypeInfo baseTypeInfo)
{
return new JsonSchemaExporterContext(typeInfo, propertyInfo, baseTypeInfo, _currentPath.ToArray());
}
private static string FormatJsonPointer(ReadOnlySpan<string> path)
{
if (!path.IsEmpty) {
System.Text.ValueStringBuilder valueStringBuilder = new System.Text.ValueStringBuilder(path.Length * 10);
try {
valueStringBuilder.Append('#');
ReadOnlySpan<string> readOnlySpan = path;
for (int i = 0; i < readOnlySpan.Length; i++) {
ReadOnlySpan<char> readOnlySpan2 = readOnlySpan[i].AsSpan();
valueStringBuilder.Append('/');
do {
int num = readOnlySpan2.IndexOfAny('~', '/');
if (num < 0) {
valueStringBuilder.Append(readOnlySpan2);
break;
}
valueStringBuilder.Append(readOnlySpan2.Slice(0, num));
if (readOnlySpan2[num] == '~')
valueStringBuilder.Append("~0");
else
valueStringBuilder.Append("~1");
readOnlySpan2 = readOnlySpan2.Slice(num + 1);
} while (!readOnlySpan2.IsEmpty);
}
return valueStringBuilder.ToString();
} finally {
valueStringBuilder.Dispose();
}
}
return "#";
}
}
public static JsonNode GetJsonSchemaAsNode(this JsonSerializerOptions options, Type type, [System.Runtime.CompilerServices.Nullable(2)] JsonSchemaExporterOptions exporterOptions = null)
{
if (options == null)
ThrowHelper.ThrowArgumentNullException("options");
if ((object)type == null)
ThrowHelper.ThrowArgumentNullException("type");
ValidateOptions(options);
return options.GetTypeInfoInternal(type, true, true, false, false).GetJsonSchemaAsNode(exporterOptions);
}
public static JsonNode GetJsonSchemaAsNode(this JsonTypeInfo typeInfo, [System.Runtime.CompilerServices.Nullable(2)] JsonSchemaExporterOptions exporterOptions = null)
{
if (typeInfo == null)
ThrowHelper.ThrowArgumentNullException("typeInfo");
ValidateOptions(typeInfo.Options);
if (exporterOptions == null)
exporterOptions = JsonSchemaExporterOptions.Default;
typeInfo.EnsureConfigured();
GenerationState state = new GenerationState(typeInfo.Options, exporterOptions);
return MapJsonSchemaCore(ref state, typeInfo, null, null, null, null, false, false, null, true).ToJsonNode(exporterOptions);
}
private static JsonSchema MapJsonSchemaCore(ref GenerationState state, JsonTypeInfo typeInfo, JsonPropertyInfo propertyInfo = null, JsonConverter customConverter = null, JsonNumberHandling? customNumberHandling = default(JsonNumberHandling?), JsonTypeInfo parentPolymorphicTypeInfo = null, bool parentPolymorphicTypeContainsTypesWithoutDiscriminator = false, bool parentPolymorphicTypeIsNonNullable = false, KeyValuePair<string, JsonSchema>? typeDiscriminator = default(KeyValuePair<string, JsonSchema>?), bool cacheResult = true)
{
<>c__DisplayClass2_0 <>c__DisplayClass2_ = default(<>c__DisplayClass2_0);
<>c__DisplayClass2_.propertyInfo = propertyInfo;
<>c__DisplayClass2_.typeInfo = typeInfo;
<>c__DisplayClass2_.parentPolymorphicTypeIsNonNullable = parentPolymorphicTypeIsNonNullable;
<>c__DisplayClass2_.exporterContext = state.CreateContext(<>c__DisplayClass2_.typeInfo, <>c__DisplayClass2_.propertyInfo, parentPolymorphicTypeInfo);
if (cacheResult && <>c__DisplayClass2_.typeInfo.Kind != 0 && state.TryGetExistingJsonPointer(ref <>c__DisplayClass2_.exporterContext, out string existingJsonPointer))
return <MapJsonSchemaCore>g__CompleteSchema|2_0(ref state, new JsonSchema {
Ref = existingJsonPointer
}, ref <>c__DisplayClass2_);
JsonConverter jsonConverter = customConverter ?? <>c__DisplayClass2_.typeInfo.Converter;
JsonNumberHandling jsonNumberHandling = customNumberHandling ?? <>c__DisplayClass2_.typeInfo.NumberHandling ?? <>c__DisplayClass2_.typeInfo.Options.NumberHandling;
JsonSchema schema = jsonConverter.GetSchema(jsonNumberHandling);
if (schema != null)
return <MapJsonSchemaCore>g__CompleteSchema|2_0(ref state, schema, ref <>c__DisplayClass2_);
if (parentPolymorphicTypeInfo == null) {
JsonPolymorphismOptions polymorphismOptions = <>c__DisplayClass2_.typeInfo.PolymorphismOptions;
if (polymorphismOptions != null) {
IList<JsonDerivedType> derivedTypes2 = polymorphismOptions.DerivedTypes;
if (derivedTypes2 != null && derivedTypes2.Count > 0) {
string typeDiscriminatorPropertyName = polymorphismOptions.TypeDiscriminatorPropertyName;
List<JsonDerivedType> list = new List<JsonDerivedType>(polymorphismOptions.DerivedTypes);
if (!<>c__DisplayClass2_.typeInfo.Type.IsAbstract && !IsPolymorphicTypeThatSpecifiesItselfAsDerivedType(<>c__DisplayClass2_.typeInfo))
list.Add(new JsonDerivedType(<>c__DisplayClass2_.typeInfo.Type));
bool flag = list.Exists((JsonDerivedType derivedTypes) => derivedTypes.TypeDiscriminator == null);
JsonSchemaType jsonSchemaType = JsonSchemaType.Any;
List<JsonSchema> list2 = new List<JsonSchema>(list.Count);
state.PushSchemaNode("anyOf");
foreach (JsonDerivedType item in list) {
KeyValuePair<string, JsonSchema>? nullable = null;
object typeDiscriminator2 = item.TypeDiscriminator;
if (typeDiscriminator2 != null) {
string text = typeDiscriminator2 as string;
JsonNode jsonNode = (text == null) ? ((JsonNode)(int)typeDiscriminator2) : ((JsonNode)text);
JsonNode constant = jsonNode;
JsonSchema value = new JsonSchema {
Constant = constant
};
nullable = new KeyValuePair<string, JsonSchema>(typeDiscriminatorPropertyName, value);
}
JsonTypeInfo typeInfoInternal = <>c__DisplayClass2_.typeInfo.Options.GetTypeInfoInternal(item.DerivedType, true, true, false, false);
state.PushSchemaNode(list2.Count.ToString(CultureInfo.InvariantCulture));
JsonTypeInfo typeInfo2 = typeInfoInternal;
JsonTypeInfo typeInfo3 = <>c__DisplayClass2_.typeInfo;
KeyValuePair<string, JsonSchema>? typeDiscriminator3 = nullable;
bool parentPolymorphicTypeContainsTypesWithoutDiscriminator2 = flag;
bool parentPolymorphicTypeIsNonNullable2 = <>c__DisplayClass2_.propertyInfo != null && !<>c__DisplayClass2_.propertyInfo.IsGetNullable && !<>c__DisplayClass2_.propertyInfo.IsSetNullable;
JsonSchema jsonSchema = MapJsonSchemaCore(ref state, typeInfo2, null, null, null, typeInfo3, parentPolymorphicTypeContainsTypesWithoutDiscriminator2, parentPolymorphicTypeIsNonNullable2, typeDiscriminator3, false);
state.PopSchemaNode();
if (list2.Count == 0)
jsonSchemaType = jsonSchema.Type;
else if (jsonSchemaType != jsonSchema.Type) {
jsonSchemaType = JsonSchemaType.Any;
}
list2.Add(jsonSchema);
}
state.PopSchemaNode();
if (jsonSchemaType != 0) {
foreach (JsonSchema item2 in list2) {
item2.Type = JsonSchemaType.Any;
if (item2.KeywordCount == 0) {
list2 = null;
break;
}
}
}
return <MapJsonSchemaCore>g__CompleteSchema|2_0(ref state, new JsonSchema {
Type = jsonSchemaType,
AnyOf = list2,
Required = (flag ? null : new List<string>(1) {
typeDiscriminatorPropertyName
})
}, ref <>c__DisplayClass2_);
}
}
}
JsonConverter nullableElementConverter = jsonConverter.NullableElementConverter;
if (nullableElementConverter == null) {
switch (<>c__DisplayClass2_.typeInfo.Kind) {
case JsonTypeInfoKind.Object: {
List<KeyValuePair<string, JsonSchema>> list3 = null;
List<string> list4 = null;
JsonSchema additionalProperties = null;
if ((<>c__DisplayClass2_.typeInfo.UnmappedMemberHandling ?? <>c__DisplayClass2_.typeInfo.Options.UnmappedMemberHandling) == JsonUnmappedMemberHandling.Disallow)
additionalProperties = JsonSchema.CreateFalseSchema();
if (typeDiscriminator.HasValue) {
KeyValuePair<string, JsonSchema> valueOrDefault2 = typeDiscriminator.GetValueOrDefault();
(list3 ?? (list3 = new List<KeyValuePair<string, JsonSchema>>())).Add(valueOrDefault2);
if (parentPolymorphicTypeContainsTypesWithoutDiscriminator)
(list4 ?? (list4 = new List<string>())).Add(valueOrDefault2.Key);
}
state.PushSchemaNode("properties");
foreach (JsonPropertyInfo property in <>c__DisplayClass2_.typeInfo.Properties) {
if ((property != null && ((property.Get == null && property.Set == null) || property.IsExtensionData)) ? true : false)
continue;
state.PushSchemaNode(property.Name);
JsonSchema schema2 = MapJsonSchemaCore(ref state, property.JsonTypeInfo, property, property.EffectiveConverter, property.EffectiveNumberHandling, null, false, false, null, true);
state.PopSchemaNode();
JsonParameterInfo associatedParameter = property.AssociatedParameter;
if (associatedParameter != null && associatedParameter.HasDefaultValue) {
JsonSchema.EnsureMutable(ref schema2);
schema2.DefaultValue = JsonSerializer.SerializeToNode(associatedParameter.DefaultValue, property.JsonTypeInfo);
schema2.HasDefaultValue = true;
}
(list3 ?? (list3 = new List<KeyValuePair<string, JsonSchema>>())).Add(new KeyValuePair<string, JsonSchema>(property.Name, schema2));
if (property == null)
goto IL_05b4;
if (!property.IsRequired) {
JsonParameterInfo associatedParameter2 = property.AssociatedParameter;
if (associatedParameter2 == null || !associatedParameter2.IsRequiredParameter)
goto IL_05b4;
}
bool parentPolymorphicTypeIsNonNullable2 = true;
goto IL_05b7;
IL_05b4:
parentPolymorphicTypeIsNonNullable2 = false;
goto IL_05b7;
IL_05b7:
if (parentPolymorphicTypeIsNonNullable2)
(list4 ?? (list4 = new List<string>())).Add(property.Name);
}
state.PopSchemaNode();
return <MapJsonSchemaCore>g__CompleteSchema|2_0(ref state, new JsonSchema {
Type = JsonSchemaType.Object,
Properties = list3,
Required = list4,
AdditionalProperties = additionalProperties
}, ref <>c__DisplayClass2_);
}
case JsonTypeInfoKind.Enumerable: {
if (!typeDiscriminator.HasValue) {
state.PushSchemaNode("items");
JsonSchema jsonSchema3 = MapJsonSchemaCore(ref state, <>c__DisplayClass2_.typeInfo.ElementTypeInfo, null, null, jsonNumberHandling, null, false, false, null, true);
state.PopSchemaNode();
return <MapJsonSchemaCore>g__CompleteSchema|2_0(ref state, new JsonSchema {
Type = JsonSchemaType.Array,
Items = (jsonSchema3.IsTrue ? null : jsonSchema3)
}, ref <>c__DisplayClass2_);
}
state.PushSchemaNode("properties");
state.PushSchemaNode("$values");
state.PushSchemaNode("items");
JsonSchema jsonSchema4 = MapJsonSchemaCore(ref state, <>c__DisplayClass2_.typeInfo.ElementTypeInfo, null, null, jsonNumberHandling, null, false, false, null, true);
state.PopSchemaNode();
state.PopSchemaNode();
state.PopSchemaNode();
return <MapJsonSchemaCore>g__CompleteSchema|2_0(ref state, new JsonSchema {
Type = JsonSchemaType.Object,
Properties = new List<KeyValuePair<string, JsonSchema>>(2) {
typeDiscriminator.Value,
new KeyValuePair<string, JsonSchema>("$values", new JsonSchema {
Type = JsonSchemaType.Array,
Items = (jsonSchema4.IsTrue ? null : jsonSchema4)
})
},
Required = (parentPolymorphicTypeContainsTypesWithoutDiscriminator ? new List<string>(1) {
typeDiscriminator.Value.Key
} : null)
}, ref <>c__DisplayClass2_);
}
case JsonTypeInfoKind.Dictionary: {
List<KeyValuePair<string, JsonSchema>> properties = null;
List<string> required = null;
if (typeDiscriminator.HasValue) {
KeyValuePair<string, JsonSchema> valueOrDefault = typeDiscriminator.GetValueOrDefault();
properties = new List<KeyValuePair<string, JsonSchema>>(1) {
valueOrDefault
};
if (parentPolymorphicTypeContainsTypesWithoutDiscriminator)
required = new List<string>(1) {
valueOrDefault.Key
};
}
state.PushSchemaNode("additionalProperties");
JsonSchema jsonSchema2 = MapJsonSchemaCore(ref state, <>c__DisplayClass2_.typeInfo.ElementTypeInfo, null, null, jsonNumberHandling, null, false, false, null, true);
state.PopSchemaNode();
return <MapJsonSchemaCore>g__CompleteSchema|2_0(ref state, new JsonSchema {
Type = JsonSchemaType.Object,
Properties = properties,
Required = required,
AdditionalProperties = (jsonSchema2.IsTrue ? null : jsonSchema2)
}, ref <>c__DisplayClass2_);
}
default:
return <MapJsonSchemaCore>g__CompleteSchema|2_0(ref state, JsonSchema.CreateTrueSchema(), ref <>c__DisplayClass2_);
}
}
JsonTypeInfo typeInfo4 = <>c__DisplayClass2_.typeInfo.Options.GetTypeInfo(nullableElementConverter.Type);
schema = MapJsonSchemaCore(ref state, typeInfo4, null, nullableElementConverter, null, null, false, false, null, false);
if (schema.Enum != null)
schema.Enum.Add(null);
return <MapJsonSchemaCore>g__CompleteSchema|2_0(ref state, schema, ref <>c__DisplayClass2_);
}
private static void ValidateOptions(JsonSerializerOptions options)
{
if (options.ReferenceHandler == ReferenceHandler.Preserve)
ThrowHelper.ThrowNotSupportedException_JsonSchemaExporterDoesNotSupportReferenceHandlerPreserve();
options.MakeReadOnly();
}
private static bool IsPolymorphicTypeThatSpecifiesItselfAsDerivedType(JsonTypeInfo typeInfo)
{
foreach (JsonDerivedType derivedType in typeInfo.PolymorphismOptions.DerivedTypes) {
if (derivedType.DerivedType == typeInfo.Type)
return true;
}
return false;
}
}
}