Schema Evaluation Program Internals
This document describes how generated types and standalone evaluators validate instances since the per-type validation emitters were retired. The evaluation engine itself is described in RuntimeEvaluator.md.
Overview
The V5 generator no longer emits validation code. Every generated type's EvaluateSchema and every
standalone evaluator's Evaluate call into a single schema evaluation program emitted once per
compilation, which the Corvus.Text.Json.RuntimeEvaluator compiles into an in-memory node graph on first
use. The typed model (accessors, conversions, builders, mutable documents, enums, constants) is unchanged;
only the JsonSchema nested class of each type shrank to an entry-point reference and a one-line
Evaluate.
This gives generated types the runtime evaluator's throughput (several times faster than the retired emitted validators on the Sourcemeta corpora), full annotation collection in verbose mode for typed models as well as standalone evaluators, and it removes roughly fourteen thousand lines of emitter code from the generator.
What is emitted
Per compilation, RuntimeProgramGenerator (src/Corvus.Text.Json.CodeGeneration/RuntimeProgramGenerator.cs)
emits one CorvusJsonSchemaProgram class in the default namespace:
- Program image or documents. When the generator host can compile ahead of time (the CLI, through
CSharpLanguageProvider.Options.ProgramCompiler), the class carries the compiled program image as base64 UTF-8 literals, decoded and loaded withJsonSchemaEvaluator.FromProgramImageon first use, plus a provider (EcmaRegexProvider) that gives the evaluator Corvus.Regex.Ecma262's compiled matcher for each pattern it matches with a regular expression, so it compiles none. The Roslyn source generator emits the same shape: it links the evaluator's compiler and image writer in as source, built overSystem.Text.Json, and Corvus.Regex.Ecma262's reader and emitter from the library's source package. With--regexEngine DotNetthe program wires one[GeneratedRegex]method per pattern throughRegexProviderinstead (the CLI only; on .NET 8 and later). A host without a program compiler emits the documents instead: every root schema document the type builder loaded, as UTF-8 static data ("..."u8). File-system documents are keyed under the syntheticcorvus-schema:///scheme, relative to the common directory of all file documents, so no build-machine path reaches the generated code; documents with an absolute$idare keyed by that$id. Relative$refs between documents resolve within the same scheme, so the keys are consistent. - Regular expressions, written once. Each distinct regular expression of a generation (a pattern with its
grammar, edition and version of Unicode) is written once, in
CorvusJsonSchemaRegularExpressionsbeside the program: Corvus.Regex.Ecma262's compiled matcher, written by the library's emitter with the compiled regular expression as data, behind an internal property so that each is built when first used. The program's provider and a generated type's pattern-property fields refer to those properties. A pattern every type of a specification carries is one matcher, not one for each type. - Regular expression match timeout. Every expression has a match timeout of 1 second
(
CodeGenerationExtensions.RegexMatchTimeoutMilliseconds, the one place the generator holds the value): each compiled matcher carries it, and the options the program is loaded with setRegexMatchTimeoutto the same value, for a pattern the evaluator compiles itself. A match that runs for longer throwsRegexMatchTimeoutExceptionto the caller. With the DotNet engine each[GeneratedRegex]carries the timeout too, except for a patternEcmaRegexLinearity.IsLinearproves linear. - Patterns. Every pattern is read as the runtime evaluator reads it: with the grammar of the
uflag, or failing that with none, in the edition of the type's dialect (ECMAScript 2020 for draft 2019-09 and draft 2020-12) unless--regexEditionsays otherwise. A pattern that is not a valid ECMA-262 regular expression stops the generation: the emitter reads everypatternandpatternPropertiespattern of a type (ThrowIfAPatternIsNotValid), and compiling the program image refuses the schema too. With the DotNet engine every pattern is the translationEcmaRegexTranslatorwrites for it (EcmaRegexTranslator.md), as before. - Entry points. One
ForEntryPoint("<document key>#<pointer>")per generated type (itsSchemaDocumentandSchemaLocation, which are still emitted as constants) and per standalone evaluator root. Entry points share one compiled program, so each subschema is compiled once even when many types reference it. - Options. The dialect for documents without
$schema(from the fallback vocabulary) and whetherformatis asserted (CorvusTextJsonAlwaysAssertFormat).
Per type, the JsonSchema class holds:
private static readonly JsonSchemaEvaluator Evaluator = CorvusJsonSchemaProgram.Entry(9);
internal static bool Evaluate(IJsonDocument parentDocument, int parentIndex, IJsonSchemaResultsCollector? resultsCollector = null)
{
return Evaluator.Evaluate(parentDocument, parentIndex, resultsCollector);
}
A standalone evaluator (EmitEvaluator = true, or the SchemaEvaluationOnly/Both generation modes)
is a <Name>Evaluator class with the same public Evaluate<TElement>(in instance, collector) entry
point as before, plus an Evaluate(IJsonDocument, int, collector) overload, over the root's entry
point.
Where the pieces live
| Concern | Location |
|---|---|
| Program and evaluator shim emission | src/Corvus.Text.Json.CodeGeneration/RuntimeProgramGenerator.cs |
| Entry-point registration, document hand-off | CSharpLanguageProvider.GetProgramEntry, ISchemaProgramLanguageProvider |
| Document collection at generation time | JsonSchemaTypeBuilder.GetSchemaDocuments (every registered LocatedSchema's root document) |
| Per-type shim | CodeGeneratorExtensions.JsonSchema.cs, AppendRuntimeProgramEvaluateMethod |
| Schema compilation and evaluation | src/Corvus.Text.Json/Corvus/Text/Json/RuntimeEvaluator in Corvus.Text.Json (design notes) |
Consumer requirements
Generated code references Corvus.Text.Json.RuntimeEvaluator, so a project that uses the source
generator or CLI output must reference that package alongside Corvus.Text.Json. Nothing else changes:
EvaluateSchema, the results collector levels, and JsonSchemaAnnotationProducer behave as before.
Roadmap
Programs emitted by the CLI are pre-compiled (see RuntimeEvaluatorPrecompilation.md for the design and measurements); programs emitted by the Roslyn source generator are still compiled from the embedded documents on first use, until the schema compiler and its document model are linked into the generator. Emitting per-node C# was measured and rejected: the fused plans in the compiler reach the same throughput without generated evaluation code.