Skip to content

Commit 920908c

Browse files
committed
update schema generation docs
1 parent ab5b7dc commit 920908c

3 files changed

Lines changed: 73 additions & 78 deletions

File tree

_docs/schema/schemagen/automatic-generation.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,17 @@ If you need to use runtime generation instead, you can disable source generation
9696
> Remember that runtime generation won't work with Native AOT.
9797
{: .prompt-warning}
9898

99+
Two additional MSBuild properties let you set project-wide defaults without repeating them on every attribute:
100+
101+
```xml
102+
<PropertyGroup>
103+
<JsonSchemaDefaultPropertyNaming>CamelCase</JsonSchemaDefaultPropertyNaming>
104+
<JsonSchemaDefaultPropertyOrder>AsDeclared</JsonSchemaDefaultPropertyOrder>
105+
</PropertyGroup>
106+
```
107+
108+
`JsonSchemaDefaultPropertyNaming` accepts any `NamingConvention` value (`AsDeclared`, `CamelCase`, `PascalCase`, `LowerSnakeCase`, `UpperSnakeCase`, `KebabCase`, `UpperKebabCase`). `JsonSchemaDefaultPropertyOrder` accepts `AsDeclared` or `ByName`. The `[GenerateJsonSchema]` attribute properties override these defaults per type.
109+
99110
## Automatic validation integration {#source-generation-validation}
100111

101112
To use the generated schemas for validation during deserialization, add the `GenerativeValidatingJsonConverter` to your serializer options:
@@ -208,3 +219,13 @@ var schema = new JsonSchemaBuilder()
208219
```
209220

210221
This gives you complete control over the schema, but you'll need to keep it in sync with your types manually.
222+
223+
## Diagnostics {#source-generation-diagnostics}
224+
225+
The source generator emits the following diagnostics:
226+
227+
| ID | Severity | Description |
228+
|----|----------|-------------|
229+
| `JSGEN001` | Error | Open generic types are not supported by the source generator. |
230+
| `JSGEN002` | Error | The `GeneratedJsonSchemas` class must be declared `partial`. |
231+
| `JSGEN003` | Warning | A type contains duplicate schema property names. |

_docs/schema/schemagen/examples/refiner.md

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,57 +10,34 @@ order: "01.06.4.4"
1010

1111
Sometimes, you may need to have custom logic that changes the generated schema in a way that can't be fulfilled with Generators, Intents, or Attributes.
1212

13-
As an example, this library handles nullability outside of these mechanisms by making use of a _refiner_.
14-
15-
This example shows how this kind of custom logic can be accomplished.
16-
17-
It first looks at the generated schema to determine whether it can add a `null` to the `type` keyword. To do this, it needs to look at a configuration option as well as a special `[Nullable(bool)]` attribute that is used to override the option.
13+
As an illustration, consider a refiner that ensures every generated string schema requires at least one character, unless a `MinLength` is already present.
1814

1915
```c#
20-
internal class NullabilityRefiner : ISchemaRefiner
16+
internal class NonEmptyStringRefiner : ISchemaRefiner
2117
{
22-
public bool ShouldRun(SchemaGeneratorContextBase context)
18+
public bool ShouldRun(SchemaGenerationContextBase context)
2319
{
24-
// we only want to run this if the generated schema has a `type` keyword
25-
return context.Intents.OfType<TypeIntent>().Any();
20+
// Only run when the schema has a string type
21+
return context.Intents.OfType<TypeIntent>()
22+
.Any(t => t.Type.HasFlag(SchemaValueType.String));
2623
}
2724

28-
public void Run(SchemaGeneratorContextBase context)
25+
public void Run(SchemaGenerationContextBase context)
2926
{
30-
// find the type keyword
31-
var typeIntent = context.Intents.OfType<TypeIntent>().First();
32-
// determine if the property has an override attribute
33-
var nullableAttribute = context.Attributes.OfType<NullableAttribute>().FirstOrDefault();
34-
var nullabilityOverride = nullableAttribute?.IsNullable;
35-
36-
// if there's an override, use it
37-
if (nullabilityOverride.HasValue)
38-
{
39-
if (nullabilityOverride.Value)
40-
typeIntent.Type |= SchemaValueType.Null;
41-
else
42-
typeIntent.Type &= ~SchemaValueType.Null;
43-
return;
44-
}
45-
46-
// otherwise, look at the options to determine what to do
47-
if (context.Configuration.Nullability.HasFlag(Nullability.AllowForNullableValueTypes) &&
48-
context.Type.IsGenericType && context.Type.GetGenericTypeDefinition() == typeof(Nullable<>))
49-
typeIntent.Type |= SchemaValueType.Null;
27+
// Skip if a MinLength is already specified
28+
if (context.Intents.OfType<MinLengthIntent>().Any()) return;
5029

51-
if (context.Configuration.Nullability.HasFlag(Nullability.AllowForReferenceTypes) &&
52-
!context.Type.IsValueType)
53-
typeIntent.Type |= SchemaValueType.Null;
30+
context.Intents.Add(new MinLengthIntent(1));
5431
}
5532
}
5633
```
5734

58-
Because this refiner is defined in the library, it's added automatically. But to include your refiner in the generation process, you'll need to add it to the `Refiners` collection in the configuration options.
35+
To include a refiner in the generation process, add it to the `Refiners` collection in the configuration.
5936

6037
```c#
6138
var configuration = new SchemaGeneratorConfiguration
6239
{
63-
Refiners = {new MyRefiner()}
40+
Refiners = { new NonEmptyStringRefiner() }
6441
};
65-
JsonSchema actual = new JsonSchemaBuilder().FromType<SomeType>(configuration);
42+
var schema = new JsonSchemaBuilder().FromType<SomeType>(configuration).Build();
6643
```

_docs/schema/schemagen/schema-generation.md

Lines changed: 39 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ You can also generate schemas using `.FromType<T>()`:
4343
var schema = new JsonSchemaBuilder().FromType<MyType>().Build();
4444
```
4545

46+
A non-generic overload, `FromType(Type, ...)`, is also available for cases where the type is not known at compile time.
47+
4648
This method uses reflection and won't work with Native AOT.
4749

4850
## IMPORTANT {#schema-schemagen-disclaimer}
@@ -78,6 +80,9 @@ All of these and more are supplied via a set of attributes that can be applied t
7880
- `MinItems`
7981
- `MaxItems`
8082
- `UniqueItems`
83+
- `AdditionalItems`
84+
- Objects
85+
- `AdditionalProperties`
8186
- All
8287
- `Id`
8388
- `Required` & `Nullable` (see below)
@@ -91,16 +96,17 @@ All of these and more are supplied via a set of attributes that can be applied t
9196
- `WriteOnly`
9297
- Conditional (see [Conditionals](./conditional-generation))
9398
- `If`
94-
- `Then`
95-
- `Else`
99+
- `IfEnum`
100+
- `IfMin`
101+
- `IfMax`
96102

97-
\* The `[Obsolete]` attribute is `System.Obsolete`. All of the others have been defined within this library. `System.ComponentModel.DataAnnotations` support is currently [in discussion](https://github.com/gregsdennis/json-everything/issues/143).
103+
\* The `[Obsolete]` attribute is `System.Obsolete`. All of the others have been defined within this library. `System.ComponentModel.DataAnnotations` support is available via the separate [Data Annotations](./data-annotations) package.
98104

99105
\*\* The `[JsonExclude]` attribute functions equivalently to `[JsonIgnore]` (see below). It is included to allow generation to skip a property or an enum member while allowing serialization to consider it.
100106

101107
\*\*\* Even though the `const` and `default` keywords in JSON Schema can accept any JSON value, because they are attributes, `[Const]` and `[Default]` can only accept values which are compile-time constants.
102108

103-
> The `System.ComponentModel.DataAnnotations` annotations are not (and likely will not be) supported by this library. Defining the above attributes separately allows alignment with JSON Schema and separation of concerns between serialization and validation.
109+
> `System.ComponentModel.DataAnnotations` attributes are not handled by this library directly. Support is provided via the separate [Data Annotations](./data-annotations) package.
104110
{: .prompt-info }
105111

106112
Simply add the attributes directly to the properties and the corresponding keywords will be added to the schema.
@@ -276,23 +282,6 @@ To this end, the `[Required]` attribute will only be represented in generated sc
276282
277283
As of v5 of this library, nullability follows the code as closely as possible, using the `[Nullable]` attribute as an override. If a property is declared as nullable (either value or reference type), it will be generated as such. Applying `[Nullable(false)]` to a nullable property will disable this behavior, while applying `[Nullable(true)]` (or just `[Nullable]`) to a non-nullable property will force nullability in the schema.
278284
279-
#### Prior to v5
280-
281-
For nullable types, it may or may not be appropriate to include `null` in the `type` keyword. JsonSchema.Net.Generation controls this behavior via the `SchemaGeneratorConfiguration.Nullability` option with individual properties being overrideable via the `[Nullable(bool)]` attribute.
282-
283-
There are four options:
284-
285-
- `Disabled` - This is the default. The resulting schemas will not have `null` in the `type` keyword unless `[Nullable(true)]` is used.
286-
- `AllowForNullableValueTypes` - This will add `null` to the `type` keyword for nullable value types (i.e. `Nullable<T>`) unless `[Nullable(false)]` is used.
287-
- `AllowForReferenceTypes` - This will add `null` to the `type` keyword for reference types unless `[Nullable(false)]` is used.
288-
- `AllowForAllTypes` - This is a combination of the previous two and will add `null` to the type keyword for any type unless `[Nullable(false)]` is used.
289-
290-
> This library was unable to [detect](https://stackoverflow.com/a/62186551/878701) whether the consuming code has nullable reference types enabled. Therefore all reference types are considered nullable.
291-
{: .prompt-info }
292-
293-
> The library makes a distinction between nullable value types and reference types because value types must be explicitly nullable. This differs from reference types which are implicitly nullable, and there's not a way (via the type itself) to make a reference type non-nullable.
294-
{: .prompt-info }
295-
296285
### Property naming {#schema-schemagen-property-names}
297286
298287
In addition to the `[JsonPropertyName]` attribute, `SchemaGeneratorConfiguration.PropertyNameResolver` allows you to define a custom method for altering property names from your code into the schema. The system will adjust property names accordingly.
@@ -307,14 +296,16 @@ SchemaGeneratorConfiguration config = new()
307296
308297
For your convenience, the `PropertyNameResolvers` static class defines a few commonly-used conventions:
309298

310-
| ResolvedName | Example |
299+
| Resolver | Example |
311300
|----------------------------------------|---------------------|
312-
| PropertyNameResolvers.CamelCase | `camelCase` |
313-
| PropertyNameResolvers.PascalCase | `PascalCase` |
314-
| PropertyNameResolvers.KebabCase | `kebab-case` |
315-
| PropertyNameResolvers.UpperKebabCase | `UPPER-KEBAB-CASE` |
316-
| PropertyNameResolvers.SnakeCase | `snake_case` |
317-
| PropertyNameResolvers.UpperSnakeCase | `UPPER_SNAKE_CASE` |
301+
| `PropertyNameResolvers.AsDeclared` | `MyProperty` |
302+
| `PropertyNameResolvers.CamelCase` | `camelCase` |
303+
| `PropertyNameResolvers.PascalCase` | `PascalCase` |
304+
| `PropertyNameResolvers.KebabCase` | `kebab-case` |
305+
| `PropertyNameResolvers.UpperKebabCase` | `UPPER-KEBAB-CASE` |
306+
| `PropertyNameResolvers.SnakeCase` | `snake_case` |
307+
| `PropertyNameResolvers.LowerSnakeCase` | `snake_case` |
308+
| `PropertyNameResolvers.UpperSnakeCase` | `UPPER_SNAKE_CASE` |
318309

319310
They can be applied directly to the configuration property:
320311

@@ -369,6 +360,8 @@ Generating a properly descriptive-while-terse name is generally hard. This libr
369360
> If you only want to handle specific types in your generator and are happy with the library's generation for others, simply return null from your generator and the library's generation will be used.
370361
{: .prompt-tip }
371362

363+
To add a `$schema` keyword to generated schemas identifying the dialect, set `SchemaGeneratorConfiguration.DefaultDialect` to the appropriate URI.
364+
372365
## Extending support {#schema-schemagen-extension}
373366

374367
The above will work most of the time, but occasionally you may find that you need some additional support. Happily, the library is configured for you to provide that support yourself.
@@ -386,9 +379,9 @@ These do not _all_ need to be implemented.
386379

387380
These are the first phase of generation. When encountering a type, the system will find the first registered generator that can handle that type. The generator then creates keyword intents (see "Intents" below). The supported types list above is merely a list of the built-in generators.
388381

389-
To create a new generator, you'll need to implement the `ISchemaGenerator` interface and register it using the `GeneratorRegistry.Register()` static method. This will insert your generator at the top of the list so that it has priority.
382+
To create a new generator, you'll need to implement the `ISchemaGenerator` interface. To register it globally, use `GeneratorRegistry.Register()`, which inserts it at the top of the list. To restrict it to a specific configuration instance, add it to `SchemaGeneratorConfiguration.Generators` instead.
390383

391-
> This means that the order your generators are registered is important: last one wins. So if you want one generator to have priority over another, register the higher priority one last.
384+
> Registration order matters: last registered wins. If multiple generators can handle a given type, the one registered last will be used.
392385
{: .prompt-warning }
393386

394387
This class doesn't need to be complex. Here's the implementation for the `BooleanSchemaGenerator`:
@@ -401,7 +394,7 @@ internal class BooleanSchemaGenerator : ISchemaGenerator
401394
return type == typeof(bool);
402395
}
403396

404-
public void AddConstraints(SchemaGeneratorContextBase context)
397+
public void AddConstraints(SchemaGenerationContextBase context)
405398
{
406399
context.Intents.Add(new TypeIntent(SchemaValueType.Boolean));
407400
}
@@ -414,7 +407,7 @@ To explain _how_ it does, we need to discuss intents.
414407

415408
### The Context Object {#schema-schemagen-context}
416409

417-
The context holds all of the data you need to determine which intents need to be applied. It is defined by a base class, `SchemaGeneratorContextBase`, and two derivations, `TypeGenerationContext` and `MemberGenerationContext`.
410+
The context holds all of the data you need to determine which intents need to be applied. It is defined by a base class, `SchemaGenerationContextBase`, and two derivations, `TypeGenerationContext` and `MemberGenerationContext`.
418411

419412
`TypeGenerationContext` represents generation of just a type (including attributes present on the type itself), whereas `MemberGenerationContext` represents generation of an object member, which will have a type (and its attributes) _and_ possibly additional attributes as a member.
420413

@@ -424,14 +417,18 @@ The context holds all of the data you need to determine which intents need to be
424417
The data exposed by contexts are:
425418

426419
- `Type` - the type for which a schema is being generated
427-
- `ReferenceCount` - the number of times this context has been used
428420
- `Intents` - the collection of intents that represent this type
429-
- `Hash` - a hash value that can be used to identify this object
421+
422+
Each context also exposes an `Apply()` method that builds and returns a `JsonSchemaBuilder` with all intents applied.
423+
424+
The class also exposes two static instances, `True` and `False`, which represent boolean schemas.
430425

431426
`MemberGenerationContext` also defines:
432427

433-
- `BasedOn` - a context on which this context builds
428+
- `BasedOn` - the `TypeGenerationContext` on which this member context builds
434429
- `Attributes` - additional attributes defined on the member
430+
- `NullableRef` - true when the member is declared as a nullable reference type
431+
- `Parameter` - the index of the generic type argument this member applies to; -1 indicates the root type
435432

436433
### Intents {#schema-schemagen-intents}
437434

@@ -475,16 +472,16 @@ The attribute itself is pretty simple. It's just a class that inherits from `At
475472
[AttributeUsage(AttributeTargets.Property)]
476473
public class MaximumAttribute : Attribute, IAttributeHandler<MaximumAttribute>
477474
{
478-
public uint Value { get; }
475+
public decimal Value { get; }
479476

480-
public MaximumAttribute(uint value)
477+
public MaximumAttribute(double value)
481478
{
482-
Value = value;
479+
Value = (decimal)value;
483480
}
484481

485-
void IAttributeHandler.AddConstraints(SchemaGeneratorContextBase context)
482+
void IAttributeHandler.AddConstraints(SchemaGenerationContextBase context, Attribute attribute)
486483
{
487-
if (!context.Type.IsNumber()) return;
484+
if (!context.Type.IsNumber() && !context.Type.IsNullableNumber()) return;
488485

489486
context.Intents.Add(new MaximumIntent(Value));
490487
}
@@ -518,8 +515,8 @@ Refiners are called after all intents have been generated for each type, recursi
518515

519516
To implement a refiner, two methods will be needed:
520517

521-
- `bool ShouldRun(SchemaGeneratorContextBase)` which determines whether the refiner needs to run for the current generation iteration.
522-
- `void Run(SchemaGeneratorContextBase)` which makes whatever modifications are needed.
518+
- `bool ShouldRun(SchemaGenerationContextBase)` which determines whether the refiner needs to run for the current generation iteration.
519+
- `void Run(SchemaGenerationContextBase)` which makes whatever modifications are needed.
523520

524521
Remember that a this point, you're stil working with intents. You can add new ones as well as modify or remove existing ones. You really have complete freedom within a refiner.
525522

0 commit comments

Comments
 (0)