Decision record 0002Accepted
Zod owns the content contract; C# mirrors it under CI enforcement
Context
ADR 0001 leaves two independent parsers reading one corpus. TypeScript and C# cannot share a type system, so the frontmatter contract exists twice. Without enforcement, a field added to the Zod schema is silently dropped by the .NET parser, and the machine surface starts answering with stale or missing data.
Decision
Zod is the source of the contract. C# mirrors it. Three independent checks make divergence a build failure rather than a runtime surprise.
astro checkvalidates the entire corpus against the Zod schemas, including referential integrity of everyreference('stack')— a dangling technology id fails the build.dotnet testparses the same real corpus (not fixtures) withPortfolio.Content. The YamlDotNet deserializer runs withoutIgnoreUnmatchedProperties, so a frontmatter field that Zod knows about and C# does not is an error, not a silent drop.- JSON Schema as a committed artefact.
content/.schema/*.jsonis generated from the Zod schemas bypnpm --filter @krzemo/web schema:emitand committed. CI runsschema:verifyto prove it is current, and the C# tests validate every parsed record, re-serialized, against it. That catches the opposite direction: a C# record that no longer satisfies the contract.
ContentSchema.Version in C# and CONTENT_SCHEMA_VERSION in TypeScript must be
equal; every content file declares schemaVersion and the parser rejects a
mismatch. That is the deliberate breaking-change lever.
What C# deliberately does not validate
Length and range constraints (min, max) are declared only in Zod.
Re-implementing them in C# would be the exact duplication this ADR exists to
prevent. C# validates structure, enums, URL and date formats, and semantic
invariants such as period.end >= period.start. Value constraints are enforced
by astro check and the JSON Schema contract test, both of which run before
anything deploys.
Consequences
- Editing a Zod schema is a three-step change: edit, regenerate the JSON Schema, mirror the C# record. CI fails if any step is skipped.
- The JSON Schema artefact is generated with a plain-string stack reference,
because Astro’s
reference()carries a Zod transform and transforms have no JSON Schema representation. The shared schema modules therefore take the reference validator as a parameter. - Zod v4’s native
z.toJSONSchemaremoves the need forzod-to-json-schema.
Alternatives rejected
Generate C# from the Zod schemas. A code generator is a build dependency, a toolchain to maintain, and a debugging surface, for a model of roughly forty fields that changes a few times a year. Enforcement is cheaper than generation at this size.
A shared IDL (Protobuf, TypeSpec) as the source, generating both. Correct at a larger scale. Here it would mean writing content schemas in a third language that neither runtime reads natively, and losing Astro’s inferred types.