Skip to content
Matthew Adams By Matthew Adams Co-Founder
Json Schema Patterns in .NET - Creating a strongly typed array

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.

Defining the type and dimension of an array

JSON schema allows you define an array whose items are of a particular type using its items property.

This would be an array of rank 1, with an unbounded size - you could put no items in it [] or any number of items [1, 2, 3, ...]

If you wish to constrain it to a particular length, then you can specify either or both of minItems and maxItems.

  • Specifying just maxItems indicates that the array can can contain up to that many items.
  • Specifying just minItems indicates that the array will have at least that many items.
  • Specifying both, and making them the same value implies an array of exactly that number of items.
  • Specifying both, and making them different values gives an upper and a lower bound to the number of items.

File: person-1d-array.json

{
  "title": "A 1-dimensional array of Person instances",
  "type": "array",
  "items": { "$ref": "./person-closed.json" },
  "minItems": 30,
  "maxItems": 30
}

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

In this case we defining an array which must contain exactly 30 PersonClosed instances.

Generate the code:

generatejsonschematypes --outputPath Model --rootNamespace JsonSchemaSample.Api person-1d-array.json
Generating: HeightEntity
Generating: ConstrainedString
Generating: Person1dArray
Generating: PersonClosed

The code generator recognizes this array pattern and generates strongly typed accessors and enumerators.

In addition, it implements IEnumerable<T> so that you can take advantage of standard LINQ operators if required.

Example code

using Corvus.Json;
using JsonSchemaSample.Api;
using NodaTime;

// Create an array of 30 people.

string peopleArrayJson =
    """
    [
      {
        "familyName": "Smith",
        "givenName": "John",
        "otherNames": "Edward,Michael",
        "birthDate": "2004-01-01",
        "height": 1.8
      },
      {
        "familyName": "Johnson",
        "givenName": "Alice",
        "birthDate": "2000-02-02",
        "height": 1.6
      },
      {
        "familyName": "Williams",
        "givenName": "Robert",
        "otherNames": "James,Thomas",
        "birthDate": "1995-03-03",
        "height": 1.7
      },
      {
        "familyName": "Brown",
        "givenName": "Jessica",
        "birthDate": "1990-04-04",
        "height": 1.9
      },
      {
        "familyName": "Jones",
        "givenName": "Michael",
        "otherNames": "Andrew,Patrick",
        "birthDate": "1985-05-05",
        "height": 1.75
      },
      {
        "familyName": "Garcia",
        "givenName": "Sarah",
        "birthDate": "1980-06-06",
        "height": 1.65
      },
      {
        "familyName": "Miller",
        "givenName": "William",
        "otherNames": "Robert,James",
        "birthDate": "1975-07-07",
        "height": 1.85
      },
      {
        "familyName": "Davis",
        "givenName": "Elizabeth",
        "birthDate": "1970-08-08",
        "height": 1.8
      },
      {
        "familyName": "Rodriguez",
        "givenName": "David",
        "otherNames": "Michael,Andrew",
        "birthDate": "1965-09-09",
        "height": 1.7
      },
      {
        "familyName": "Martinez",
        "givenName": "Jennifer",
        "birthDate": "1960-10-10",
        "height": 1.75
      },
      {
        "familyName": "Hernandez",
        "givenName": "Joseph",
        "otherNames": "Robert,James",
        "birthDate": "1955-11-11",
        "height": 1.8
      },
      {
        "familyName": "Lopez",
        "givenName": "Emily",
        "birthDate": "1950-12-12",
        "height": 1.85
      },
      {
        "familyName": "Gonzalez",
        "givenName": "James",
        "birthDate": "1945-01-13",
        "height": 1.9
      },
      {
        "familyName": "Wilson",
        "givenName": "Emma",
        "otherNames": "Elizabeth,Mary",
        "birthDate": "1940-02-14",
        "height": 1.95
      },
      {
        "familyName": "Anderson",
        "givenName": "Thomas",
        "birthDate": "1935-03-15",
        "height": 1.8
      },
      {
        "familyName": "Thomas",
        "givenName": "Mary",
        "otherNames": "Elizabeth,Anne",
        "birthDate": "1930-04-16",
        "height": 1.6
      },
      {
        "familyName": "Taylor",
        "givenName": "Christopher",
        "birthDate": "1935-05-17",
        "height": 1.7
      },
      {
        "familyName": "Moore",
        "givenName": "Patricia",
        "otherNames": "Anne,Elizabeth",
        "birthDate": "1940-06-18",
        "height": 1.9
      },
      {
        "familyName": "Jackson",
        "givenName": "Daniel",
        "birthDate": "1945-07-19",
        "height": 1.75
      },
      {
        "familyName": "Martin",
        "givenName": "Hannah",
        "birthDate": "1950-08-20",
        "height": 1.65
      },
      {
        "familyName": "Lee",
        "givenName": "Matthew",
        "otherNames": "James,Andrew",
        "birthDate": "1955-09-21",
        "height": 1.85
      },
      {
        "familyName": "Perez",
        "givenName": "Barbara",
        "birthDate": "1960-10-22",
        "height": 1.8
      },
      {
        "familyName": "Thompson",
        "givenName": "Anthony",
        "otherNames": "Robert,James",
        "birthDate": "1965-11-23",
        "height": 1.7
      },
      {
        "familyName": "White",
        "givenName": "Amanda",
        "birthDate": "1970-12-24",
        "height": 1.75
      },
      {
        "familyName": "Harris",
        "givenName": "Andrew",
        "otherNames": "Michael,Patrick",
        "birthDate": "1975-01-25",
        "height": 1.8
      },
      {
        "familyName": "Clark",
        "givenName": "Megan",
        "birthDate": "1980-02-26",
        "height": 1.85
      },
      {
        "familyName": "Lewis",
        "givenName": "Joshua",
        "otherNames": "Robert,James",
        "birthDate": "1985-03-27",
        "height": 1.9
      },
      {
        "familyName": "Robinson",
        "givenName": "Rebecca",
        "otherNames": "Elizabeth,Mary",
        "birthDate": "1990-04-28",
        "height": 1.95
      },
      {
        "familyName": "Walker",
        "givenName": "Ethan",
        "birthDate": "1995-05-29",
        "height": 1.8
      },
      {
        "familyName": "Hall",
        "givenName": "Julia",
        "otherNames": "Anne,Elizabeth",
        "birthDate": "2000-06-30",
        "height": 1.6
      }
    ]
    """;

using var peopleArrayParsed = ParsedValue<Person1dArray>.Parse(peopleArrayJson);
Person1dArray peopleArray = peopleArrayParsed.Instance;

// Access strongly-typed values by array index
PersonClosed personAtIndex0 = peopleArray[0];
PersonClosed personAtIndex1 = peopleArray[1];

if (peopleArray.IsValid())
{
    // The array is valid - there are 30 valid PersonClosed instances in it.
    Console.WriteLine("original array is valid.");
}
else
{
    Console.WriteLine("original array is not valid.");
}

// Item removed at index
Person1dArray updatedArray = peopleArray.RemoveAt(0);
// Remove first instance of a specific value
updatedArray = updatedArray.Remove(personAtIndex1);

if (updatedArray.IsValid())
{
    Console.WriteLine("Updated array is valid.");
}
else
{
    // The array is no longer valid - the length is 28
    Console.WriteLine("Updated array is not valid.");
}

// Insert an item at an index
updatedArray = updatedArray.Insert(0, personAtIndex1);
// Add an item at the end
updatedArray = updatedArray.Add(personAtIndex0);

if (updatedArray.IsValid())
{
    // The array is valid - the length is back up to 30
    Console.WriteLine("Updated array is valid.");
}
else
{
    Console.WriteLine("Updated array is not valid.");
}

PersonClosed personToSet = PersonClosed.Create(
    birthDate: new LocalDate(1820, 1, 17),
    familyName: "Brontë",
    givenName: "Anne",
    height: 1.57);

updatedArray = updatedArray.SetItem(14, personToSet);

if (updatedArray[14].Equals(personToSet))
{
    // The person was replaced at position 14
    Console.WriteLine("Person was set at position 14.");
}
else
{
    Console.WriteLine("Person was not set at position 14.");
}

// Replace the first instance of a person.
updatedArray = updatedArray.Replace(personAtIndex0, personToSet);

// Enumerate the items in the array
foreach(PersonClosed enumeratedPerson in updatedArray.EnumerateArray())
{
    Console.WriteLine($"{enumeratedPerson.FamilyName}, {enumeratedPerson.GivenName}");
}

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.