Skip to content

Commit de709b1

Browse files
Add JsonSchemaExporter. (#103322)
* Add JsonSchemaExporter. * Address feedback * Address feedback * Address feedback. * Add pattern keyword to numeric converters supporting string serialization. * Remove type keyword from string enum schemas. * Add $comment annotations to schemas using pattern. * Use transformer delegate instead of mutator. * Add TreatNullObliviousAsNonNullable setting. * Additional comments.
1 parent 7e9cab2 commit de709b1

File tree

61 files changed

+3077
-9
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+3077
-9
lines changed

eng/Versions.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,8 @@
186186
<NUnitVersion>3.12.0</NUnitVersion>
187187
<NUnit3TestAdapterVersion>4.5.0</NUnit3TestAdapterVersion>
188188
<CoverletCollectorVersion>6.0.0</CoverletCollectorVersion>
189+
<SystemComponentModelAnnotationsVersion>5.0.0</SystemComponentModelAnnotationsVersion>
190+
<JsonSchemaNetVersion>7.0.2</JsonSchemaNetVersion>
189191
<NewtonsoftJsonVersion>13.0.3</NewtonsoftJsonVersion>
190192
<NewtonsoftJsonBsonVersion>1.0.2</NewtonsoftJsonBsonVersion>
191193
<SQLitePCLRawbundle_greenVersion>2.0.4</SQLitePCLRawbundle_greenVersion>

src/libraries/System.Text.Json/ref/System.Text.Json.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -888,6 +888,29 @@ internal JsonValue() { }
888888
public abstract bool TryGetValue<T>([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out T? value);
889889
}
890890
}
891+
namespace System.Text.Json.Schema
892+
{
893+
public static partial class JsonSchemaExporter
894+
{
895+
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; }
896+
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; }
897+
}
898+
public readonly partial struct JsonSchemaExporterContext
899+
{
900+
private readonly object _dummy;
901+
private readonly int _dummyPrimitive;
902+
public System.Text.Json.Serialization.Metadata.JsonPropertyInfo? PropertyInfo { get { throw null; } }
903+
public System.ReadOnlySpan<string> Path { get { throw null; } }
904+
public System.Text.Json.Serialization.Metadata.JsonTypeInfo TypeInfo { get { throw null; } }
905+
}
906+
public sealed partial class JsonSchemaExporterOptions
907+
{
908+
public JsonSchemaExporterOptions() { }
909+
public static System.Text.Json.Schema.JsonSchemaExporterOptions Default { get { throw null; } }
910+
public System.Func<JsonSchemaExporterContext, System.Text.Json.Nodes.JsonNode, System.Text.Json.Nodes.JsonNode>? TransformSchemaNode { get { throw null; } init { } }
911+
public bool TreatNullObliviousAsNonNullable { get { throw null; } init { } }
912+
}
913+
}
891914
namespace System.Text.Json.Serialization
892915
{
893916
public partial interface IJsonOnDeserialized

src/libraries/System.Text.Json/src/Resources/Strings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,4 +752,10 @@
752752
<data name="NullabilityInfoContext_NotSupported" xml:space="preserve">
753753
<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>
754754
</data>
755+
<data name="JsonSchemaExporter_ReferenceHandlerPreserve_NotSupported" xml:space="preserve">
756+
<value>JSON schema generation is not supported for contracts using ReferenceHandler.Preserve.</value>
757+
</data>
758+
<data name="JsonSchemaExporter_DepthTooLarge" xml:space="preserve">
759+
<value>The depth of the generated JSON schema exceeds the JsonSerializerOptions.MaxDepth setting.</value>
760+
</data>
755761
</root>

src/libraries/System.Text.Json/src/System.Text.Json.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ The System.Text.Json library is built-in as part of the shared framework in .NET
100100
<Compile Include="System\Text\Json\Reader\Utf8JsonReader.cs" />
101101
<Compile Include="System\Text\Json\Reader\Utf8JsonReader.MultiSegment.cs" />
102102
<Compile Include="System\Text\Json\Reader\Utf8JsonReader.TryGet.cs" />
103+
<Compile Include="System\Text\Json\Schema\JsonSchema.cs" />
104+
<Compile Include="System\Text\Json\Schema\JsonSchemaExporter.cs" />
105+
<Compile Include="System\Text\Json\Schema\JsonSchemaExporterOptions.cs" />
106+
<Compile Include="System\Text\Json\Schema\JsonSchemaExporterContext.cs" />
107+
<Compile Include="System\Text\Json\Schema\JsonSchemaType.cs" />
103108
<Compile Include="System\Text\Json\Serialization\Arguments.cs" />
104109
<Compile Include="System\Text\Json\Serialization\ArgumentState.cs" />
105110
<Compile Include="System\Text\Json\Serialization\Attributes\JsonObjectCreationHandlingAttribute.cs" />
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections.Generic;
5+
using System.Diagnostics;
6+
using System.Text.Json.Nodes;
7+
8+
namespace System.Text.Json.Schema
9+
{
10+
internal sealed class JsonSchema
11+
{
12+
internal const string RefPropertyName = "$ref";
13+
internal const string CommentPropertyName = "$comment";
14+
internal const string TypePropertyName = "type";
15+
internal const string FormatPropertyName = "format";
16+
internal const string PatternPropertyName = "pattern";
17+
internal const string PropertiesPropertyName = "properties";
18+
internal const string RequiredPropertyName = "required";
19+
internal const string ItemsPropertyName = "items";
20+
internal const string AdditionalPropertiesPropertyName = "additionalProperties";
21+
internal const string EnumPropertyName = "enum";
22+
internal const string NotPropertyName = "not";
23+
internal const string AnyOfPropertyName = "anyOf";
24+
internal const string ConstPropertyName = "const";
25+
internal const string DefaultPropertyName = "default";
26+
internal const string MinLengthPropertyName = "minLength";
27+
internal const string MaxLengthPropertyName = "maxLength";
28+
29+
public static JsonSchema False { get; } = new(false);
30+
public static JsonSchema True { get; } = new(true);
31+
32+
public JsonSchema() { }
33+
private JsonSchema(bool trueOrFalse) { _trueOrFalse = trueOrFalse; }
34+
35+
public bool IsTrue => _trueOrFalse is true;
36+
public bool IsFalse => _trueOrFalse is false;
37+
private readonly bool? _trueOrFalse;
38+
39+
public string? Ref { get => _ref; set { VerifyMutable(); _ref = value; } }
40+
private string? _ref;
41+
42+
public string? Comment { get => _comment; set { VerifyMutable(); _comment = value; } }
43+
private string? _comment;
44+
45+
public JsonSchemaType Type { get => _type; set { VerifyMutable(); _type = value; } }
46+
private JsonSchemaType _type = JsonSchemaType.Any;
47+
48+
public string? Format { get => _format; set { VerifyMutable(); _format = value; } }
49+
private string? _format;
50+
51+
public string? Pattern { get => _pattern; set { VerifyMutable(); _pattern = value; } }
52+
private string? _pattern;
53+
54+
public JsonNode? Constant { get => _constant; set { VerifyMutable(); _constant = value; } }
55+
private JsonNode? _constant;
56+
57+
public List<KeyValuePair<string, JsonSchema>>? Properties { get => _properties; set { VerifyMutable(); _properties = value; } }
58+
private List<KeyValuePair<string, JsonSchema>>? _properties;
59+
60+
public List<string>? Required { get => _required; set { VerifyMutable(); _required = value; } }
61+
private List<string>? _required;
62+
63+
public JsonSchema? Items { get => _items; set { VerifyMutable(); _items = value; } }
64+
private JsonSchema? _items;
65+
66+
public JsonSchema? AdditionalProperties { get => _additionalProperties; set { VerifyMutable(); _additionalProperties = value; } }
67+
private JsonSchema? _additionalProperties;
68+
69+
public JsonArray? Enum { get => _enum; set { VerifyMutable(); _enum = value; } }
70+
private JsonArray? _enum;
71+
72+
public JsonSchema? Not { get => _not; set { VerifyMutable(); _not = value; } }
73+
private JsonSchema? _not;
74+
75+
public List<JsonSchema>? AnyOf { get => _anyOf; set { VerifyMutable(); _anyOf = value; } }
76+
private List<JsonSchema>? _anyOf;
77+
78+
public bool HasDefaultValue { get => _hasDefaultValue; set { VerifyMutable(); _hasDefaultValue = value; } }
79+
private bool _hasDefaultValue;
80+
81+
public JsonNode? DefaultValue { get => _defaultValue; set { VerifyMutable(); _defaultValue = value; } }
82+
private JsonNode? _defaultValue;
83+
84+
public int? MinLength { get => _minLength; set { VerifyMutable(); _minLength = value; } }
85+
private int? _minLength;
86+
87+
public int? MaxLength { get => _maxLength; set { VerifyMutable(); _maxLength = value; } }
88+
private int? _maxLength;
89+
90+
public JsonSchemaExporterContext? ExporterContext { get; set; }
91+
92+
public int KeywordCount
93+
{
94+
get
95+
{
96+
if (_trueOrFalse != null)
97+
{
98+
return 0;
99+
}
100+
101+
int count = 0;
102+
Count(Ref != null);
103+
Count(Comment != null);
104+
Count(Type != JsonSchemaType.Any);
105+
Count(Format != null);
106+
Count(Pattern != null);
107+
Count(Constant != null);
108+
Count(Properties != null);
109+
Count(Required != null);
110+
Count(Items != null);
111+
Count(AdditionalProperties != null);
112+
Count(Enum != null);
113+
Count(Not != null);
114+
Count(AnyOf != null);
115+
Count(HasDefaultValue);
116+
Count(MinLength != null);
117+
Count(MaxLength != null);
118+
119+
return count;
120+
121+
void Count(bool isKeywordSpecified)
122+
{
123+
count += isKeywordSpecified ? 1 : 0;
124+
}
125+
}
126+
}
127+
128+
public void MakeNullable()
129+
{
130+
if (_trueOrFalse != null)
131+
{
132+
return;
133+
}
134+
135+
if (Type != JsonSchemaType.Any)
136+
{
137+
Type |= JsonSchemaType.Null;
138+
}
139+
}
140+
141+
public JsonNode ToJsonNode(JsonSchemaExporterOptions options)
142+
{
143+
if (_trueOrFalse is { } boolSchema)
144+
{
145+
return CompleteSchema((JsonNode)boolSchema);
146+
}
147+
148+
var objSchema = new JsonObject();
149+
150+
if (Ref != null)
151+
{
152+
objSchema.Add(RefPropertyName, Ref);
153+
}
154+
155+
if (Comment != null)
156+
{
157+
objSchema.Add(CommentPropertyName, Comment);
158+
}
159+
160+
if (MapSchemaType(Type) is JsonNode type)
161+
{
162+
objSchema.Add(TypePropertyName, type);
163+
}
164+
165+
if (Format != null)
166+
{
167+
objSchema.Add(FormatPropertyName, Format);
168+
}
169+
170+
if (Pattern != null)
171+
{
172+
objSchema.Add(PatternPropertyName, Pattern);
173+
}
174+
175+
if (Constant != null)
176+
{
177+
objSchema.Add(ConstPropertyName, Constant);
178+
}
179+
180+
if (Properties != null)
181+
{
182+
var properties = new JsonObject();
183+
foreach (KeyValuePair<string, JsonSchema> property in Properties)
184+
{
185+
properties.Add(property.Key, property.Value.ToJsonNode(options));
186+
}
187+
188+
objSchema.Add(PropertiesPropertyName, properties);
189+
}
190+
191+
if (Required != null)
192+
{
193+
var requiredArray = new JsonArray();
194+
foreach (string requiredProperty in Required)
195+
{
196+
requiredArray.Add((JsonNode)requiredProperty);
197+
}
198+
199+
objSchema.Add(RequiredPropertyName, requiredArray);
200+
}
201+
202+
if (Items != null)
203+
{
204+
objSchema.Add(ItemsPropertyName, Items.ToJsonNode(options));
205+
}
206+
207+
if (AdditionalProperties != null)
208+
{
209+
objSchema.Add(AdditionalPropertiesPropertyName, AdditionalProperties.ToJsonNode(options));
210+
}
211+
212+
if (Enum != null)
213+
{
214+
objSchema.Add(EnumPropertyName, Enum);
215+
}
216+
217+
if (Not != null)
218+
{
219+
objSchema.Add(NotPropertyName, Not.ToJsonNode(options));
220+
}
221+
222+
if (AnyOf != null)
223+
{
224+
JsonArray anyOfArray = [];
225+
foreach (JsonSchema schema in AnyOf)
226+
{
227+
anyOfArray.Add(schema.ToJsonNode(options));
228+
}
229+
230+
objSchema.Add(AnyOfPropertyName, anyOfArray);
231+
}
232+
233+
if (HasDefaultValue)
234+
{
235+
objSchema.Add(DefaultPropertyName, DefaultValue);
236+
}
237+
238+
if (MinLength is int minLength)
239+
{
240+
objSchema.Add(MinLengthPropertyName, (JsonNode)minLength);
241+
}
242+
243+
if (MaxLength is int maxLength)
244+
{
245+
objSchema.Add(MaxLengthPropertyName, (JsonNode)maxLength);
246+
}
247+
248+
return CompleteSchema(objSchema);
249+
250+
JsonNode CompleteSchema(JsonNode schema)
251+
{
252+
if (ExporterContext is { } context)
253+
{
254+
Debug.Assert(options.TransformSchemaNode != null, "context should only be populated if a callback is present.");
255+
// Apply any user-defined transformations to the schema.
256+
return options.TransformSchemaNode(context, schema);
257+
}
258+
259+
return schema;
260+
}
261+
}
262+
263+
private static ReadOnlySpan<JsonSchemaType> s_schemaValues =>
264+
[
265+
// NB the order of these values influences order of types in the rendered schema
266+
JsonSchemaType.String,
267+
JsonSchemaType.Integer,
268+
JsonSchemaType.Number,
269+
JsonSchemaType.Boolean,
270+
JsonSchemaType.Array,
271+
JsonSchemaType.Object,
272+
JsonSchemaType.Null,
273+
];
274+
275+
private void VerifyMutable()
276+
{
277+
Debug.Assert(_trueOrFalse is null, "Schema is not mutable");
278+
if (_trueOrFalse is not null)
279+
{
280+
Throw();
281+
static void Throw() => throw new InvalidOperationException();
282+
}
283+
}
284+
285+
public static JsonNode? MapSchemaType(JsonSchemaType schemaType)
286+
{
287+
if (schemaType is JsonSchemaType.Any)
288+
{
289+
return null;
290+
}
291+
292+
if (ToIdentifier(schemaType) is string identifier)
293+
{
294+
return identifier;
295+
}
296+
297+
var array = new JsonArray();
298+
foreach (JsonSchemaType type in s_schemaValues)
299+
{
300+
if ((schemaType & type) != 0)
301+
{
302+
array.Add((JsonNode)ToIdentifier(type)!);
303+
}
304+
}
305+
306+
return array;
307+
308+
static string? ToIdentifier(JsonSchemaType schemaType)
309+
{
310+
return schemaType switch
311+
{
312+
JsonSchemaType.Null => "null",
313+
JsonSchemaType.Boolean => "boolean",
314+
JsonSchemaType.Integer => "integer",
315+
JsonSchemaType.Number => "number",
316+
JsonSchemaType.String => "string",
317+
JsonSchemaType.Array => "array",
318+
JsonSchemaType.Object => "object",
319+
_ => null,
320+
};
321+
}
322+
}
323+
}
324+
}

0 commit comments

Comments
 (0)