Dynamic Schema Validation
Overview
Corvus.Text.Json.Validator is a library for loading and validating JSON documents against JSON Schema at runtime. Unlike the build-time source generator, the Validator compiles schemas into an in-memory evaluator with the runtime evaluator — no code generation and no Roslyn — making it ideal for scenarios where schemas are not known at compile time, such as schema registries, configuration validation, or user-supplied schemas.
It supports all major JSON Schema drafts (Draft 4, 6, 7, 2019-09, and 2020-12), including custom metaschemas that declare their vocabularies with $vocabulary.
Key Features
- Millisecond Cold Start: Schemas compile into an in-memory evaluator in milliseconds, with no code generation and no compiler in the process
- Zero-Allocation Validation: Once warm, pass/fail validation of a parsed document allocates nothing
- Multiple Schema Drafts: Supports Draft 4, 6, 7, 2019-09, and 2020-12 with automatic draft detection
- Schema Caching: Compiled schemas are cached so repeated validations against the same schema are fast
- Multiple Input Formats: Validate from strings, byte arrays, streams,
ReadOnlyMemory,ReadOnlySequence, or pre-parsedJsonElement - Detailed Diagnostics: Optional results collector provides hierarchical validation failures with schema locations and error messages
- External Schema Resolution: Resolve
$refreferences via file system, HTTP, or pre-loaded additional schema files
Installation
dotnet add package Corvus.Text.Json.Validator
Quick Start
The main entry point is the JsonSchema struct. Load a schema, then validate JSON documents against it:
using Corvus.Text.Json.Validator;
// Load a schema from a file
JsonSchema schema = JsonSchema.FromFile("Schemas/person.json");
// Validate a JSON string
bool isValid = schema.Validate("""{"name": "Alice", "age": 30}""");
Loading Schemas
JsonSchema provides several factory methods for loading schemas from different sources:
From a File
JsonSchema schema = JsonSchema.FromFile("path/to/schema.json");
The file's location is the base URI for resolving relative $ref references unless the schema declares an absolute $id. The schema is cached under its normalised file path.
From a String
string schemaText = """
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/person",
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" },
"age": { "type": "integer", "minimum": 0 }
}
}
""";
JsonSchema schema = JsonSchema.FromText(schemaText);
If the schema does not contain an $id property, you must provide a canonical URI explicitly:
JsonSchema schema = JsonSchema.FromText(schemaText, canonicalUri: "https://example.com/person");
From a Stream
using FileStream stream = File.OpenRead("schema.json");
JsonSchema schema = JsonSchema.FromStream(stream);
From a URI
Resolve a schema by its canonical URI. The document is retrieved through the additionalDocumentResolver, the additionalSchemaFiles, the embedded standard metaschemas, and then the file system or HTTP, depending on the URI scheme. A fragment selects a subschema within the document:
JsonSchema schema = JsonSchema.FromUri("https://example.com/schemas/person.json");
// Or use the shorthand alias
JsonSchema schema = JsonSchema.From("file:///C:/schemas/person.json");
// Validate against a subschema of the document
JsonSchema address = JsonSchema.FromUri("https://example.com/schemas/person.json#/$defs/address");
Validating Documents
Once you have a JsonSchema, call Validate() with the JSON document in any supported format:
// From a string
bool valid = schema.Validate("""{"name": "Alice"}""");
// From UTF-8 bytes
ReadOnlyMemory<byte> utf8 = Encoding.UTF8.GetBytes("""{"name": "Alice"}""");
bool valid = schema.Validate(utf8);
// From a stream (e.g., HTTP request body)
bool valid = schema.Validate(requestStream);
// From a pre-parsed JsonElement
using var doc = ParsedJsonDocument<JsonElement>.Parse(json);
bool valid = schema.Validate(in doc.RootElement);
All overloads return true if the document is valid, false otherwise.
Detailed Validation Results
For diagnostic output, pass an IJsonSchemaResultsCollector to Validate(). The collector records every validation step, including the schema location and evaluation path for failures:
using JsonSchemaResultsCollector collector =
JsonSchemaResultsCollector.Create(JsonSchemaResultsLevel.Detailed);
bool valid = schema.Validate(json, collector);
if (!valid)
{
foreach (var result in collector.EnumerateResults())
{
if (!result.IsMatch)
{
Console.WriteLine($" Failed: {result}");
}
}
}
Results Levels
| Level | What It Collects |
|---|---|
Basic |
Failure messages without location information |
Detailed |
Failure messages with schema location and evaluation path |
Verbose |
All evaluation steps, including successful validations |
Omit the collector for maximum performance when you only need a boolean result: flag validation fails fast and allocates nothing. Use Detailed or Verbose when diagnosing schema violations.
Configuration Options
Pass a JsonSchema.Options instance to any factory method to control schema compilation behaviour:
var options = new JsonSchema.Options(
alwaysAssertFormat: true,
allowFileSystemAndHttpResolution: true,
defaultDialect: JsonSchemaDialect.Draft202012,
additionalSchemaFiles: new[]
{
new AdditionalSchemaFile(
canonicalUri: "https://example.com/shared/address.json",
filePath: "Schemas/address.json")
});
JsonSchema schema = JsonSchema.FromFile("person.json", options: options);
Available Options
| Option | Default | Description |
|---|---|---|
alwaysAssertFormat |
true |
When true, the format keyword is enforced as a validation assertion. When false, it is treated as an annotation only (per the JSON Schema specification). |
allowFileSystemAndHttpResolution |
true |
Enable resolution of $ref references via file:// and http:///https:// URIs. Set to false to restrict resolution to pre-loaded schemas only. |
defaultDialect |
JsonSchemaDialect.Draft202012 |
The JSON Schema dialect to use when the schema does not include a $schema keyword. |
additionalSchemaFiles |
null |
Pre-load external schema files for $ref resolution. Each entry maps a canonical URI to a local file path; the file is also registered under its $id and its full path. |
additionalDocumentResolver |
null |
A JsonSchemaDocumentResolver delegate that supplies referenced documents from memory, consulted before the file system and HTTP. |
Pre-loading Referenced Schemas
When your schema uses $ref to reference other schemas, you can pre-load them with AdditionalSchemaFile:
var options = new JsonSchema.Options(
additionalSchemaFiles: new[]
{
new AdditionalSchemaFile(
"https://example.com/schemas/address.json",
"Schemas/address.json"),
new AdditionalSchemaFile(
"https://example.com/schemas/phone.json",
"Schemas/phone.json")
});
JsonSchema schema = JsonSchema.FromFile("Schemas/person.json", options: options);
This avoids network calls for referenced schemas and ensures deterministic builds. It is particularly useful in CI/CD environments where external resolution may be unreliable or disallowed.
Schema Caching
Compiled schemas are cached automatically by their canonical URI and alwaysAssertFormat flag. Subsequent calls to any From* method with the same URI return the cached validator without recompilation:
// First call: compiles the schema (milliseconds)
JsonSchema schema1 = JsonSchema.FromFile("person.json");
// Second call: returns the cached evaluator
JsonSchema schema2 = JsonSchema.FromFile("person.json");
To force recompilation (for example, after updating a schema file), pass refreshCache: true:
JsonSchema schema = JsonSchema.FromFile("person.json", refreshCache: true);
How It Works
The Validator is a thin wrapper over the runtime evaluator:
- Load the JSON Schema document and any documents it references, using the registered resolvers
- Compile the schema graph into an in-memory evaluator: every
$refis resolved at compile time, regular expressions are compiled, and flag-mode fast paths (discriminators, type unions, unrolled objects) are precomputed - Cache the evaluator by canonical URI and
alwaysAssertFormatfor subsequent validations against the same schema
Validation walks the parsed document once against the compiled graph. It shares the format, number, and string helpers used by generated code, so it agrees with source-generated models instance for instance, and it produces the same results-collector output and annotations. No hosting configuration is required: the Validator has no dependency on Roslyn or on the host's compilation context.
Supported JSON Schema Drafts
| Draft | $schema URI |
|---|---|
| Draft 4 | http://json-schema.org/draft-04/schema |
| Draft 6 | http://json-schema.org/draft-06/schema |
| Draft 7 | http://json-schema.org/draft-07/schema |
| Draft 2019-09 | https://json-schema.org/draft/2019-09/schema |
| Draft 2020-12 | https://json-schema.org/draft/2020-12/schema |
Custom metaschemas are supported when they declare their vocabularies with $vocabulary; metaschemas without $vocabulary (Draft 7 and earlier) fall back to the dialect selected by $schema or defaultDialect.
Use Cases
- Schema Registry Validation: Validate messages against schemas fetched from a central registry at runtime
- Configuration Validation: Validate user-supplied configuration files against application-defined schemas
- API Gateway Validation: Validate HTTP request/response bodies against OpenAPI schemas
- Testing: Validate test fixtures against schemas without pre-generating types
- Dynamic Schema Selection: Choose schemas based on runtime conditions (e.g., document version, content type)