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
Annotationtype is aref structwhose spans reference the internal buffers of theJsonSchemaResultsCollector. 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:
CollectAnnotationsallocates dictionaries. For production use, preferEnumerateAnnotationsorWriteAnnotationsTo.
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
- Source Generator — build-time type generation
- CLI Code Generator — command-line type generation
- Validator — runtime validation API