Skip to content

Standalone Schema Evaluator

Overview

The standalone schema evaluator is an alternative code generation mode that produces a lightweight static evaluator class for JSON Schema validation and annotation collection, without generating the full strongly-typed C# models.

This is ideal for scenarios where you need:

  • Full annotation collection — conformant JSON Schema annotation gathering for tooling that consumes annotations (e.g., form generators, documentation tools, schema-driven UIs)
  • Smaller footprint — the evaluator generates a single class per schema instead of a type hierarchy, reducing binary size and compilation time
  • Validation-only workflows — when you need schema validation but don't require serialization, property accessors, or builder support

The evaluator supports all the same JSON Schema drafts as the type-based generator (Draft 4, 6, 7, 2019-09, 2020-12, and OpenAPI 3.0).

Using the Source Generator

Add the source generator and runtime packages to your project:

<PackageReference Include="Corvus.Text.Json.SourceGenerator" Version="5.0.0">
  <PrivateAssets>all</PrivateAssets>
  <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Corvus.Text.Json" Version="5.0.0" />
<PackageReference Include="Corvus.Text.Json.RuntimeEvaluator" Version="5.0.0" />

Annotate a partial struct with EmitEvaluator = true:

[JsonSchemaTypeGenerator("Schemas/person.json", EmitEvaluator = true)]
public readonly partial struct Person;

This generates both the strongly-typed Person type and a standalone PersonEvaluator class. Both validate through the same schema evaluation program; the evaluator is useful when you want to evaluate any IJsonElement<T> without going through the typed model.

If you only want the evaluator (no types), use the CLI tool with --codeGenerationMode SchemaEvaluationOnly.

Using the CLI Tool

# Generate only the standalone evaluator (no types)
corvusjson jsonschema Schemas/person.json \
    --rootNamespace MyApp.Evaluators \
    --outputPath Generated/ \
    --codeGenerationMode SchemaEvaluationOnly

# Generate both types and evaluator
corvusjson jsonschema Schemas/person.json \
    --rootNamespace MyApp.Models \
    --outputPath Generated/ \
    --codeGenerationMode Both

Code generation modes

Mode Description
TypeGeneration Generate strongly-typed C# models (default)
SchemaEvaluationOnly Generate only the standalone evaluator class
Both Generate both types and the standalone evaluator

What Gets Generated

The evaluator is a small static class with the public Evaluate<TElement>(in instance, resultsCollector) entry point (and an Evaluate(IJsonDocument, int, resultsCollector) overload). It delegates to the assembly's schema evaluation program, CorvusJsonSchemaProgram, which is emitted once per compilation with one entry point per schema. Programs generated by the CLI and by the Roslyn source generator carry the program compiled ahead of time: an image loaded by Corvus.Text.Json.RuntimeEvaluator on first use, with [GeneratedRegex] for its patterns, holding the in-memory node graph with every $ref resolved and flag-mode fast paths precomputed. Evaluation then walks the parsed instance once against that graph, allocating nothing.

Generated types use the same program: EvaluateSchema() on a typed model is an entry point of the same graph, so validation and annotation results are identical whichever entry you use. Projects that consume generated code must reference Corvus.Text.Json.RuntimeEvaluator. See StandaloneEvaluatorInternals.md for the emitted shape.

Annotation Collection

The standalone evaluator provides fully compliant annotation collection conforming to the JSON Schema specification. By contrast, the type-based generator only collects annotations for validation keywords.

To collect annotations, run the evaluator with a JsonSchemaResultsCollector in Verbose mode, then use JsonSchemaAnnotationProducer to extract the annotations.

Basic enumeration with foreach

The EnumerateAnnotations method returns a zero-allocation ref struct enumerator that you can use in a foreach loop:

using Corvus.Text.Json;

// Parse the instance
using var doc = ParsedJsonDocument<JsonElement>.Parse(jsonText);
JsonElement instance = doc.RootElement;

// Validate in Verbose mode
using var collector = JsonSchemaResultsCollector.Create(JsonSchemaResultsLevel.Verbose);
instance.EvaluateSchema(collector);

// Enumerate annotations
foreach (JsonSchemaAnnotationProducer.Annotation annotation
    in JsonSchemaAnnotationProducer.EnumerateAnnotations(collector))
{
    // Each annotation is a ref struct with UTF-8 span properties:
    //   annotation.InstanceLocation  — e.g. "", "/foo", "/items/0"
    //   annotation.Keyword           — e.g. "title", "description", "default"
    //   annotation.SchemaLocation    — e.g. "", "/$defs/foo"
    //   annotation.Value             — raw JSON value, e.g. "\"My Title\"", "42", "true"

    // String accessors are also available:
    Console.WriteLine(
        $"  {annotation.GetInstanceLocationText()} " +
        $"[{annotation.GetKeywordText()}] " +
        $"@ {annotation.GetSchemaLocationText()} " +
        $"= {annotation.GetValueText()}");
}

Note: The Annotation type is a ref struct whose spans reference the internal buffers of the JsonSchemaResultsCollector. It is only valid during enumeration and must not be stored beyond the current iteration. Use the string accessors (GetKeywordText(), etc.) if you need to capture values.

Writing annotations as JSON

WriteAnnotationsTo writes all annotations as a structured JSON object to a Utf8JsonWriter. The output is grouped by instance location, then by keyword, then by schema location:

using var collector = JsonSchemaResultsCollector.Create(JsonSchemaResultsLevel.Verbose);
instance.EvaluateSchema(collector);

using var buffer = new MemoryStream();
using (var writer = new Utf8JsonWriter(buffer, new JsonWriterOptions { Indented = true }))
{
    JsonSchemaAnnotationProducer.WriteAnnotationsTo(collector, writer);
}

// Output structure:
// {
//   "": {                          // instance location (root)
//     "title": {
//       "#": "\"Person\""          // schema location → annotation value
//     },
//     "description": {
//       "#": "\"A person object\""
//     }
//   },
//   "/name": {
//     "title": {
//       "#/properties/name": "\"Full name\""
//     }
//   }
// }

Callback-based enumeration

For scenarios where you want to process annotations without a foreach loop, use the callback overload. Return true to continue, false to stop early:

JsonSchemaAnnotationProducer.EnumerateAnnotations(
    collector,
    (instanceLocation, keyword, schemaLocation, annotationValue) =>
    {
        Console.WriteLine($"{instanceLocation}/{keyword} @ {schemaLocation} = {annotationValue}");
        return true; // continue enumeration
    });

Collecting annotations into a dictionary (testing)

The CollectAnnotations method returns a Dictionary keyed by (instanceLocation, keyword), useful for testing assertions:

var annotations = JsonSchemaAnnotationProducer.CollectAnnotations(collector);

// Check a specific annotation exists
Assert.IsTrue(annotations.TryGetValue(("", "title"), out var titleMap));
Assert.AreEqual("\"Person\"", titleMap["#"]);

Note: CollectAnnotations allocates dictionaries. For production use, prefer EnumerateAnnotations or WriteAnnotationsTo.

Performance Optimizations

The evaluator inherits every optimisation of the runtime evaluator: direct metadata-row access into parsed documents, per-node fused evaluation plans, oneOf/anyOf discriminators, type unions, unrolled small objects, fused simple arrays, plain-integer bounds, pattern classification (.*, .+, ^prefix, ^.{n,m}$ never allocate a Regex), compiled regexes with a process-wide cache, and compile-time elision of pure $ref hops. The optimisation table is in RuntimeEvaluator.md.

Comparison with Type-Based Generation

Feature Type Generation Evaluator Only
Strongly-typed accessors ✅ ❌
JSON serialization/deserialization ✅ ❌
Mutable builder support ✅ ❌
Implicit/explicit conversions ✅ ❌
Schema validation ✅ ✅
Annotation collection Fully compliant Fully compliant
Binary size Larger (model code) Smaller
Compilation time Longer (model code) Shorter

Validation itself is the same engine in both modes; the difference is only whether the typed model is emitted.

See Also