Skip to content

Add JsonSchemaExporter. #103322

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 15, 2024
Merged
2 changes: 2 additions & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@
<NUnitVersion>3.12.0</NUnitVersion>
<NUnit3TestAdapterVersion>4.5.0</NUnit3TestAdapterVersion>
<CoverletCollectorVersion>6.0.0</CoverletCollectorVersion>
<SystemComponentModelAnnotationsVersion>5.0.0</SystemComponentModelAnnotationsVersion>
<JsonSchemaNetVersion>7.0.2</JsonSchemaNetVersion>
<NewtonsoftJsonVersion>13.0.3</NewtonsoftJsonVersion>
<NewtonsoftJsonBsonVersion>1.0.2</NewtonsoftJsonBsonVersion>
<SQLitePCLRawbundle_greenVersion>2.0.4</SQLitePCLRawbundle_greenVersion>
Expand Down
23 changes: 23 additions & 0 deletions src/libraries/System.Text.Json/ref/System.Text.Json.cs
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,29 @@ internal JsonValue() { }
public abstract bool TryGetValue<T>([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<string> 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<JsonSchemaExporterContext, System.Text.Json.Nodes.JsonNode, System.Text.Json.Nodes.JsonNode>? TransformSchemaNode { get { throw null; } init { } }
public bool TreatNullObliviousAsNonNullable { get { throw null; } init { } }
}
}
namespace System.Text.Json.Serialization
{
public partial interface IJsonOnDeserialized
Expand Down
6 changes: 6 additions & 0 deletions src/libraries/System.Text.Json/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -752,4 +752,10 @@
<data name="NullabilityInfoContext_NotSupported" xml:space="preserve">
<value>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.</value>
</data>
<data name="JsonSchemaExporter_ReferenceHandlerPreserve_NotSupported" xml:space="preserve">
<value>JSON schema generation is not supported for contracts using ReferenceHandler.Preserve.</value>
</data>
<data name="JsonSchemaExporter_DepthTooLarge" xml:space="preserve">
<value>The depth of the generated JSON schema exceeds the JsonSerializerOptions.MaxDepth setting.</value>
</data>
</root>
5 changes: 5 additions & 0 deletions src/libraries/System.Text.Json/src/System.Text.Json.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
<Compile Include="System\Text\Json\Reader\Utf8JsonReader.cs" />
<Compile Include="System\Text\Json\Reader\Utf8JsonReader.MultiSegment.cs" />
<Compile Include="System\Text\Json\Reader\Utf8JsonReader.TryGet.cs" />
<Compile Include="System\Text\Json\Schema\JsonSchema.cs" />
<Compile Include="System\Text\Json\Schema\JsonSchemaExporter.cs" />
<Compile Include="System\Text\Json\Schema\JsonSchemaExporterOptions.cs" />
<Compile Include="System\Text\Json\Schema\JsonSchemaExporterContext.cs" />
<Compile Include="System\Text\Json\Schema\JsonSchemaType.cs" />
<Compile Include="System\Text\Json\Serialization\Arguments.cs" />
<Compile Include="System\Text\Json\Serialization\ArgumentState.cs" />
<Compile Include="System\Text\Json\Serialization\Attributes\JsonObjectCreationHandlingAttribute.cs" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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<KeyValuePair<string, JsonSchema>>? Properties { get => _properties; set { VerifyMutable(); _properties = value; } }
private List<KeyValuePair<string, JsonSchema>>? _properties;

public List<string>? Required { get => _required; set { VerifyMutable(); _required = value; } }
private List<string>? _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<JsonSchema>? AnyOf { get => _anyOf; set { VerifyMutable(); _anyOf = value; } }
private List<JsonSchema>? _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<string, JsonSchema> 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<JsonSchemaType> 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,
};
}
}
}
}
Loading
Loading