Skip to content
Matthew Adams By Matthew Adams Co-Founder · 2 min read
Introducing Corvus.Text.Json V5: Schema Validation - 10× Faster

At endjin, we maintain Corvus.JsonSchema, and in the previous post we saw how the source generator produces complete typed models from JSON Schema.

Now let's look at what happens when you call person.EvaluateSchema().

Validation is not optional

If you're working with JSON that crosses a trust boundary - API requests, message queues, configuration files, user-supplied data - you need to validate it. Not "check a couple of fields" validate. Schema validate. Against a specification that defines what valid means, and that both producer and consumer agree on.

JSON Schema is that specification, and it covers all the major drafts: 4, 6, 7, 2019-09, and 2020-12 - plus OpenAPI 3.0 and 3.1.

V5 validates over 10× faster than other .NET JSON Schema validators.

The generated validator

If you've followed the source generator approach from the previous post, validation is already built in:

using var doc = ParsedJsonDocument<Person>.Parse(
    """{"name":"Alice","age":30}""");
Person person = doc.RootElement;

bool valid = person.EvaluateSchema();  // true

That's it. The source generator produced a validation method tailored to your specific schema, with every keyword compiled into type-specific checks. There is no interpretation overhead and no runtime schema walking.

Getting diagnostic output

When validation fails and you need to know why, pass a results collector:

using var collector = JsonSchemaResultsCollector.Create(
    JsonSchemaResultsLevel.Detailed);

bool valid = person.EvaluateSchema(collector);

if (!valid)
{
    foreach (var result in collector.EnumerateResults())
    {
        if (!result.IsMatch)
        {
            Console.WriteLine(
                $"  Failed: {result.GetMessageText()} " +
                $"at {result.GetDocumentEvaluationLocationText()}");
        }
    }
}

When you call EvaluateSchema() without a collector, you get a simple pass/fail bool with maximum performance. If you need diagnostics, create a JsonSchemaResultsCollector and choose how much detail you want:

Level What it collects
Basic Failure messages only (lowest overhead)
Detailed Failures with schema location and evaluation path
Verbose Every evaluation step, including successes

Use the no-collector overload in production hot paths; use Detailed or Verbose for diagnostics.

Dynamic validation at runtime

Not every scenario has schemas known at compile time. Schema registries, configuration validators, and user-supplied schemas all need runtime compilation.

The Corvus.Text.Json.Validator package handles this. Internally, it uses the same code generation engine as the source generator, but invokes Roslyn dynamically:

using Corvus.Text.Json.Validator;

// Load a schema - from a file, string, stream, or URI
JsonSchema schema = JsonSchema.FromFile("Schemas/person.json");

// Validate
bool valid = schema.Validate("""{"name": "Alice", "age": 30}""");

Compiled schemas are cached automatically by their canonical URI, so the first validation incurs a compilation cost (around 100ms) but subsequent calls are sub-millisecond.

Multiple input formats

The Validate() method accepts JSON in whatever format you already have it:

// UTF-8 bytes (zero copy from a network buffer)
bool valid = schema.Validate(utf8Bytes);

// Stream (HTTP request body, file stream)
bool valid = schema.Validate(stream);

// Pre-parsed element
using var doc = ParsedJsonDocument<JsonElement>.Parse(json);
bool valid = schema.Validate(in doc.RootElement);

Pre-loading referenced schemas

If your schema uses $ref to reference other schema files, you can pre-load them to avoid file system or network resolution:

var options = new JsonSchema.Options(
    additionalSchemaFiles: new[]
    {
        new AdditionalSchemaFile(
            "https://example.com/schemas/address.json",
            "Schemas/address.json")
    });

JsonSchema schema = JsonSchema.FromFile("person.json", options: options);

This is particularly useful in CI/CD environments where external resolution may be unreliable or disallowed.

Conformance

We take correctness seriously. The V5 engine passes the official JSON Schema Test Suite for all supported drafts, and we publish results to Bowtie - the cross-implementation conformance project.

Bowtie runs every JSON Schema implementation against the same test suite and publishes comparative results. It's the definitive way to verify a validator's correctness, and we encourage you to check the latest results for any implementation you're evaluating.

Next up

We've seen validation from the caller's perspective. In the next post, we'll go deeper into the V5 memory model. We'll look at how ParsedJsonDocument<T> uses ArrayPool to achieve just 136 bytes per document.

FAQs

Which JSON Schema drafts does V5 support? Draft 4, 6, 7, 2019-09, and 2020-12, plus OpenAPI 3.0 and 3.1. The draft is auto-detected from the $schema keyword.
What is the difference between EvaluateSchema() and the Validator package? EvaluateSchema() is the build-time generated validation on your typed structs. The Corvus.Text.Json.Validator package compiles schemas dynamically at runtime using Roslyn - useful when schemas are not known at compile time.
How detailed are the validation diagnostics? The results collector provides hierarchical validation failures with schema location, evaluation path, and keyword-level error messages. You can choose between Basic, Detailed, and Verbose output levels, or call EvaluateSchema() without a collector for a simple pass/fail bool.

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.