Skip to content
Matthew Adams By Matthew Adams Co-Founder
Json Schema Patterns in .NET - Open vs. Closed 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 dotnet 8.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.

Extensibility with open and closed types

Open and closed types in JSON Schema

  • Open Types

    In this context, an open type is an object type that is still valid when it contains properties other than those explicitly defined in its own schema (such as those under the properties keyword).

    By default, JSON Schema assumes an open content model, meaning that unless specified otherwise, objects can have additional properties not declared in the schema.

    This is useful when you want your objects to be extensible or when you don’t want to strictly validate every property.

    Allowing this extensibility is a common approach to "versioning by evolution" or "backwards compatibility" where it is intended that new versions of a schema are still accepted by old consumers that don't know about (or use) the new extensions.

  • Closed Types

    If you want to create a closed type in JSON Schema, you would set unevaluatedProperties to false.

    This means that the object can only contain properties that are explicitly defined in the schema, and no others.

    This is commonly used in "parallel versioning" where old consumers do not support new versions of schema, and typically new versions have new names to avoid confusion.

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
    }
  }
}

Generate the code:

generatejsonschematypes --outputPath Model --rootNamespace JsonSchemaSample.Api person-open.json
Generating: ConstrainedString
Generating: PersonOpen
Generating: HeightEntity

However, you can close the type by specifying "unevaluatedProperties": false

Prior to draft 2020-12, unevaluatedProperties was not available and you would use additionalProperties. The semantics are slightly different (and a little complex!), and you would generally prefer unevaluatedProperties today.

This will ensure that a JSON document containing properties not explicitly declared in the schema will not be valid.

File: person-closed.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
    }
  },

  "unevaluatedProperties": false,

  "$defs": {
    "constrainedString": {
      "type": "string",
      "minLength": 1,
      "maxLength": 256
    }
  }
}

Generate the code:

generatejsonschematypes --outputPath Model --rootNamespace JsonSchemaSample.Api person-closed.json

Generating: ConstrainedString
Generating: PersonClosed
Generating: HeightEntity

Example code

using Corvus.Json;
using JsonSchemaSample.Api;

// A person with an additional property: "JobRole".
var extendedPersonJsonString =
    """
    {
        "familyName": "Brontë",
        "givenName": "Anne",
        "birthDate": "1820-01-17",
        "height": 1.57,
        "jobRole": "Author"
    }
    """;

using var parsedPersonOpen = ParsedValue<PersonOpen>.Parse(extendedPersonJsonString);
PersonOpen personOpen = parsedPersonOpen.Instance;

// An object is, by default, open - allowing undeclared properties.
if(personOpen.IsValid())
{
    Console.WriteLine("personOpen is valid");
}
else
{
    Console.WriteLine("personOpen is not valid");
}

using var parsedPersonClosed = ParsedValue<PersonClosed>.Parse(extendedPersonJsonString);
PersonClosed personClosed = parsedPersonClosed.Instance;

// An object with unevaluatedProperties: false does not allow undeclared properties.
if (personClosed.IsValid())
{
    Console.WriteLine("personClosed is valid");
}
else
{
    Console.WriteLine("personClosed is not valid");
}

// We can still use an invalid entity - and fix it; for instance, by removing the invalid property
PersonClosed personFixed = personClosed.RemoveProperty("jobRole"u8);
if (personFixed.IsValid())
{
    Console.WriteLine("personFixed is valid");
}
else
{
    Console.WriteLine("personFixed is not valid");
}

// Equally we can make a valid entity invalid
// Setting an unknown property to a particular value.
PersonClosed personBroken = personFixed.SetProperty<JsonInteger>("age", 23);
if (personBroken.IsValid())
{
    Console.WriteLine("personBroken is valid");
}
else
{
    Console.WriteLine("personBroken is not valid");
}

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.