JSON is arguably one of the most used data formats on the planet, likely due to its flexibility and human readability. We can also recognize that its two greatest strengths can also be the formatโ€™s weakness. Being human-readable gives developers the confidence to make manual changes, likely making errors along the way. The flexibility of parsers tends to let these structural errors exist in production settings longer than they should.

Luckily for .NET developers, we have mechanisms to programmatically check the validity of JSON to ensure some level of correctness. Weโ€™ll see how to read JSON and check it against its schema definition.

Whatโ€™s JSON Schema?

JSON schema attempts to help define a clear expectation for JSON objects. The schema describes the format of JSON in a clear human-readable and machine-readable format. Users of JSON Schema can use it to perform structural validation ideal for validating incoming client requests and automating integration tests by generating test input.

Most modern editors support JSON schema by placing a $schema property at the start of a JSON model. Letโ€™s see an example of JSON schema.

{
  "$schema": "http://json-schema.org/draft-04/schema#",
  "title": "Product",
  "description": "A product from Acme's catalog",
  "type": "object",
  "properties": {
    "id": {
      "description": "The unique identifier for a product",
      "type": "integer"
    },
    "name": {
      "description": "Name of the product",
      "type": "string"
    },
    "price": {
      "type": "number",
      "minimum": 0,
      "exclusiveMinimum": true
    },
    "tags": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "minItems": 1,
      "uniqueItems": true
    }
  },
  "required": ["id", "name", "price"]
}

Some notable features of JSON Schema include:

  • Descriptions of each property in a JSON object
  • Type expectations for each property
  • Value expectations for each property
  • Requiring fields
  • JSON Schemas can also be validated using JSON Schema

Weโ€™ll be using this schema later in our example .NET application.

Validating JSON using .NET and NJsonSchema

Before we get started, weโ€™ll need an empty console application with the NuGet package of NJsonSchema installed. While other packages are available for JSON schema validation, Iโ€™ve found this to be the most straightforward to use.

> dotnet add NJsonSchema

Our goal is to read the $schema property from an existing JSON file and then output the validation results to the console. Weโ€™ll also be using Spectre.Console for nice display output, but simple Console.WriteLine would work just as well.

Our sample will have three different JSON documents: valid.json, empty.json, and schemaless.json. The variants of JSON objects will help us see different results. Weโ€™re reading the schema.json file from disk, but we could just as quickly read it from a remote URL, which is usually the case.

// empty.json
{
  "$schema": "schema.json"
}
// schemaless.json
{
  "wild": "west"
}
// valid.json
{
  "$schema": "schema.json",
  "id": 1,
  "name": "",
  "price": 149.00
}

Now that we have all three JSON files and our schema, we can write some code.

using System.IO;
using System.Linq;
using Newtonsoft.Json.Linq;
using NJsonSchema;
using Spectre.Console;

var files = new[] { "valid.json", "empty.json", "schemaless.json"};

var table = new Table().RoundedBorder()
    .AddColumn("๐Ÿ“ file name")
    .AddColumn("๐Ÿšจ errors");

foreach (var file in files)
{
    var text = await File.ReadAllTextAsync(file);
    var json = JToken.Parse(text);
    
    // use the schema on the json model
    var jsonSchema = json["$schema"]?.ToString();
    var schema = jsonSchema switch {
        {Length: > 0} when jsonSchema.StartsWith("http") => 
            await JsonSchema.FromUrlAsync(jsonSchema),
        {Length: > 0} =>
            await JsonSchema.FromFileAsync(jsonSchema),
        _ => null
    };

    if (schema is null)
    {
        table.AddRow(file, "[purple]unavailable $schema[/]");
        continue;
    }
    
    var errors = schema.Validate(json);
    var results = errors.Any()
        ? $"โ€ฃ {errors.Count} total errors\n" +
          string.Join("", errors
              .Select(e => $"  โ€ฃ [red]{e}[/] at " +
                           $"[yellow]{e.LineNumber}:{e.LinePosition}[/]\n"))
        : "[green]โœ”[/] [lime]looks good![/]";

    table.AddRow(file, results);
}

AnsiConsole.Render(table);

As we can see, we parse our JSON files and load the schema as we go through each file. Loading schema per file gives us the ultimate flexibility to validate each file independently. In situations where all JSON models are identical, I would recommend loading the JSON schema once.

Running our console application, we get the expected results. (The results look much nicer locally).

โ•ญโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฎ
โ”‚ ๐Ÿ“ file name    โ”‚ ๐Ÿšจ errors                            โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚ valid.json      โ”‚ โœ” looks good!                        โ”‚
โ”‚ empty.json      โ”‚ โ€ฃ 3 total errors                     โ”‚
โ”‚                 โ”‚   โ€ฃ PropertyRequired: #/id at 1:1    โ”‚
โ”‚                 โ”‚   โ€ฃ PropertyRequired: #/name at 1:1  โ”‚
โ”‚                 โ”‚   โ€ฃ PropertyRequired: #/price at 1:1 โ”‚
โ”‚                 โ”‚                                      โ”‚
โ”‚ schemaless.json โ”‚ unavailable $schema                  โ”‚
โ•ฐโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ•ฏ

Awesome!

Using JsonSchema.NET To Validate JSON

Recently, Iโ€™ve also found out how to use JsonSchema.NET, which has support for System.Text.Json elements. If youโ€™re already using System.Text.Json, which you likely are if youโ€™re on any version of .NET 5+ or higher, youโ€™ll likely want to use this package. An important note about JsonSchema.NET, is it only has support for drafts 6,8,2019-09, and 2020-12. The above schema must be modified, especially the exclusiveMinimum field, which is now an number.

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Product",
  "description": "A product from Acme's catalog",
  "type": "object",
  "properties": {
    "id": {
      "description": "The unique identifier for a product",
      "type": "integer"
    },
    "name": {
      "description": "Name of the product",
      "type": "string"
    },
    "price": {
      "type": "number",
      "minimum": 0,
      "exclusiveMinimum": 0
    },
    "tags": {
      "type": "array",
      "items": {
        "type": "string"
      },
      "minItems": 1,
      "uniqueItems": true
    }
  },
  "required": ["id", "name", "price"]
}

Letโ€™s use the above schema, with the code found below.

using System;
using System.IO;
using System.Text.Json;
using Json.Schema;

var schema = JsonSchema.FromFile("schema.json");
var file = File.OpenRead("data.json");
var json = await JsonDocument.ParseAsync(file);

var result = schema.Validate(json.RootElement);
var validity = result.IsValid ? "Valid" : "Invalid";

Console.WriteLine($"Json is {validity}");

With a few lines of code we are now validating JSON documents. Yay! You can also choose the level of validation youโ€™d like by passing a ValidationOptions parameter to Validate.

var result = schema.Validate(json.RootElement, new ValidationOptions {
    RequireFormatValidation = true,
    ValidateAs = Draft.Draft7,
    OutputFormat = OutputFormat.Verbose
});

Conclusion

JSON is everywhere, and the ability to validate an otherwise flexible data format is excellent. JSON Schema allows us to enforce a structure that we can use to give users feedback. Many editors already support the $schema property, giving us real-time validation issues. Using something like NJsonSchema allows us to use an existing schema to add a layer of validation that can reduce problems before they get out of hand. If we donโ€™t have a schema, then thatโ€™s no problem, as the JSON schema format is easier to write and maintain.

I hope you enjoyed this blog post. If you did, please share it with your friends and coworkers.