Skip to content
Matthew Adams By Matthew Adams Co-Founder
Json Schema Patterns in .NET - Reusing Common Types

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.

Using common types by reference

In our previous example, we constrained all of our types to a string of length 1-256.

File: person.json

{
  "title": "The person schema https://schema.org/Person",
  "type": "object",
  "required": [ "familyName", "givenName", "birthDate" ],
  "properties": {
    "familyName": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "givenName": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "otherNames": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    },
    "birthDate": {
      "type": "string",
      "format": "date"
    },
    "height": {
      "type": "number",
      "format": "double",
      "minimum": 0.0,
      "maximum": 3.0
    }
  }
}

We can simplify this by re-using a reference to a common schema with the $ref keyword.

File: person-common-schema.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
    }
  }
}

Generate the code:

generatejsonschematypes --outputPath Model --rootNamespace JsonSchemaSample.Api person-common-schema.json
Generating: ConstrainedString
Generating: PersonCommonSchema
Generating: HeightEntity

Instead of the three types: GivenNameEntity, FamilyNameEntity, and OtherNamesEntity, we have a single type: ConstrainedString.

Example code

using JsonSchemaSample.Api;
using NodaTime;

var personCommonSchema = PersonCommonSchema.Create(
    birthDate: new LocalDate(1820, 1, 17),
    familyName: "Brontë",
    givenName: "Anne",
    // Invalid string length
    otherNames: string.Empty,
    height: 1.57);

// Custom types can be converted in the same way as the primitive types on which they are based.
PersonCommonSchema.ConstrainedString constrainedGivenName = personCommonSchema.GivenName;
PersonCommonSchema.ConstrainedString constrainedFamilyName = personCommonSchema.FamilyName;
string cgn = (string)constrainedGivenName;
string cfn = (string)constrainedFamilyName;

// Low-allocation comparisons are available in the usual way (as are all the other features of JsonString).
constrainedGivenName.Equals(constrainedFamilyName);
constrainedGivenName.EqualsString("Hello");
constrainedGivenName.EqualsUtf8Bytes("Anne"u8);

Matthew Adams

Co-Founder

Matthew Adams

Matthew was CTO of a venture-backed technology start-up in the UK & US for 10 years, and is now the co-founder of endjin, which provides technology strategy, experience and development services to its customers who are seeking to take advantage of Microsoft Azure and the Cloud.