Json Schema Patterns in .NET - Extending a base type
In this series we are cataloging common patterns with JSON Schema and the .NET code generated by Corvus.JsonSchema.
It is especially useful for developers who want to move to a schema-first approach, but are unsure how that will map to their .NET code.
We are focusing on draft 2020-12 and the dotnet8.0 code generation, but very similar patterns apply for older versions of both .NET and draft 2019-09, draft 7, and even draft 6. We will highlight the key differences as we go.
If you have no experience of JSON Schema at all, I would recommend you read the getting started step-by-step documentation provided by the JSON Schema team.
Adding properties
When you have an open type, you can extend it with additional properties. This is rather like deriving from an (unsealed) base type in an object-oriented language, and adding additional properties in the derived type.
In this case we use $ref
to declare that we are basing a new schema on the person-open.json
schema.
We then extend it with an additional wealth
property, which is a 32-bit integer number. This happens to be declared as a required
property.
File: person-wealthy.json
{
"title": "A wealthy person",
"$ref": "./person-open.json",
"required": ["wealth"],
"properties": {
"wealth": {
"type": "integer",
"format": "int32"
}
}
}
File: person-open.json
{
"title": "The person schema https://schema.org/Person",
"type": "object",
"required": [ "familyName", "givenName", "birthDate" ],
"properties": {
"familyName": { "$ref": "#/$defs/constrainedString" },
"givenName": { "$ref": "#/$defs/constrainedString" },
"otherNames": { "$ref": "#/$defs/constrainedString" },
"birthDate": {
"type": "string",
"format": "date"
},
"height": {
"type": "number",
"format": "double",
"exclusiveMinimum": 0.0,
"maximum": 3.0
}
},
"$defs": {
"constrainedString": {
"type": "string",
"minLength": 1,
"maxLength": 256
}
}
}
In draft 6 and draft 7, $ref
cannot be used in this way. It acts as a reference to the target type and ignores the adjacent values. This is often a bit surprising! Instead, you can use allOf
with a single value to get the same effect. It is a little clunkier to read, but works in the same way.
{ "allOf": [{"$ref": "./person-closed.json"}]}
Generate the code:
generatejsonschematypes --outputPath Model --rootNamespace JsonSchemaSample.Api person-wealthy.json
Generating: HeightEntity
Generating: PersonOpen
Generating: PersonWealthy
Generating: ConstrainedString
You can see that it generates a type for both PersonOpen
and PersonWealthy
.
PersonWealthy
includes all of the properties defined both on person
and personWealthy
.
BirthDate
FamilyName
GivenName
OtherNames
Height
andWealth
using Corvus.Json;
using JsonSchemaSample.Api;
using NodaTime;
// A wealthy person has an additional required property: "Wealth".
var wealthyPerson = PersonWealthy.Create(
birthDate: new LocalDate(1820, 1, 17),
familyName: "Brontë",
givenName: "Anne",
wealth: 1_000_000,
height: 1.57);
Console.WriteLine($"Wealth: {wealthyPerson.Wealth}");
// Implicit conversion to the type on which it is based (PersonOpen) is available.
PersonOpen basePerson = wealthyPerson;
// Although the property is not present on the PersonOpen .NET type, it is still available
// through the generic TryGetProperty() API. Note that we can use the JsonPropertyNames declared
// on PersonWealthy to get the appropriate name. We know this is backed by a .NET type, so it is
// marginally more efficient to use the string version of the property name. If we were backed by a
// JsonElement, it would be marginally more efficient to use the UTF8 property name.
if (basePerson.TryGetProperty(PersonWealthy.JsonPropertyNames.Wealth, out JsonInt32 baseWealth))
{
// We will only get here if baseWealth is not undefined
// We know our PersonOpen was derived from a valid
// PersonWealthy so we do not need to re-validate; if it was
// present, then it is known to be an int32.
Console.WriteLine($"Wealth: {(int)baseWealth}");
}
// Or we could just use a literal string as the property name.
if (basePerson.TryGetProperty("wealth", out JsonInt32 baseWealth2))
{
Console.WriteLine($"Wealth: {(int)baseWealth2}");
}
// Or a literal utf8 string as the property name.
if (basePerson.TryGetProperty("wealth"u8, out JsonInt32 baseWealth3))
{
Console.WriteLine($"Wealth: {(int)baseWealth3}");
}