Skip to content

Schema Validation at the SDK Boundary — ADR-099

Schema Validation at the SDK Boundary — ADR-099

Date: 2026-08-06 · Written in English per the 2026-08-01 convention change.

Provenance: the first exercise of the technology-supersession authorisation. On 2026-08-06 the project owner recorded that where a newer technology is judged better but conflicts with the whitepaper, the whitepaper may be changed. §12.2.1’s requirement that modders “must define Zod schemas” was the first claim raised against that authorisation, and it is the reason this file exists rather than a chore(deps).

The interesting part is that the upgrade turned out not to be the answer. The question arrived as “Zod or Valibot”; both candidate answers were wrong in the same way, and the useful output is a boundary rather than a library.

This file continues the R-numbered sequence for filing purposes only.


Status: Accepted · Date: 2026-08-06 · Depends-on: 098 (server-side re-validation at ingestion, which is what makes a creator’s choice of validator a non-trust input) · interacts with 010, 011

Context

§12.2.1 states that modders “must define Zod schemas to register reactive data stores”. That sentence is the SDK’s only reference to a validation library, and it has three independent problems, of which only the first is the one that was reported.

1. It names a library in a published contract. @descent-vtt/sdk is distributed on NPM to third-party creators. A named library in a contract is a dependency every consumer inherits, and replacing it later is a major version of the SDK — the same shape as the cartridge SDK-major problem ADR-055 spends five clauses containing. The whitepaper’s own rule is that a technology with more than one consumer needs a swap point, and this one has as many consumers as there are creators.

2. The 2026 ecosystem made the choice unnecessary. Standard Schema is a small TypeScript interface — not a library — designed by the authors of Zod, Valibot and ArkType so that a tool can accept any of them without an adapter. Zod v4, Valibot, ArkType and TypeBox all conform. A framework that adopts the interface stops having an opinion, and neither party maintains an integration. The choice this ADR was convened to make is a choice the ecosystem stopped requiring.

3. The bundle argument, which prompted the question, is real but is not the deciding one. Valibot’s modular design produces materially smaller bundles than a monolithic validator, and this ships to a browser client. That is a reason to recommend Valibot. It is not a reason to mandate it, because the cost of mandating falls on creators who already know a different library, and creator adoption is the SDK’s entire purpose.

One thing this ADR is not. Design_Docs_Conflict_Scan.md R2-F-40 already repudiated a claim that “strict Zod Schemas guarantee zero-trust security for community mods”. That finding disposed of a security claim and says nothing about which validator to use. The two must not be conflated: schema validation at the SDK boundary is a correctness and developer-experience tool, and it has never been an isolation mechanism. Isolation is §6.4’s trust tiers and §4.3’s budgets, and it is unaffected by everything below.

Decision

  1. The SDK accepts any Standard Schema-conformant validator and names none in its contract. descent.state registration takes a value satisfying the ~standard interface; the SDK calls it and reads its result. Zod v4, Valibot, ArkType and TypeBox are all admissible, and so is anything that conforms later.
  2. Valibot is the documented default in SDK templates, the CLI scaffold and every example. A default is not a requirement, and the distinction is stated in the SDK reference rather than left to be inferred from the examples: a creator who reaches for Zod is not doing something the platform discourages.
  3. The SDK bundles no validator. The chosen library is the creator’s dependency, so a creator using none pays for none, and the SDK’s own footprint does not move when the recommendation does.
  4. Creator-side validation is advisory in exactly the sense ADR-098 already establishes for Studio. Whatever a creator validates locally is re-validated server-side at ingestion and at command admission, under authoritative quantities. The platform never treats a creator’s schema as a trust input — which is precisely why clause 1 is safe, and clause 1 would not be safe without it.
  5. A validator that stops conforming is a creator-side break, not a platform break. The SDK’s obligation is to state the interface version it accepts and to fail a registration with a legible error naming the interface, never to special-case a library.

Alternatives Considered and Why Rejected

  • Keep Zod, as §12.2.1 says. Rejected. This was evaluated on its merits rather than dismissed as stale: Zod v4 substantially narrowed the bundle-size gap that motivated the question, it is the library most creators already know, and “do nothing” has zero migration cost. What it does not fix is problem 1 — the contract still names a library, and the next time the ecosystem moves this same ADR gets written again. The defect is the naming, not the name.
  • Mandate Valibot. Rejected. It swaps one named library for another and re-incurs the identical problem the moment something better appears, while adding a migration cost for every existing consumer. It also mandates the less familiar option in a creator-facing API, which trades adoption for bytes without saying so.
  • Ship an SDK-owned schema abstraction with adapters per library. Rejected. It is Standard Schema, written by us, maintained by us, and worse: it puts the platform on the hook for an adapter per library and per major version. The seam exists and is already implemented by every candidate; building a second one is the “capability described twice” failure ADR-017 rejects in a different domain.
  • Require JSON Schema instead, reusing the published UI-descriptor schema machinery of §6.4. Rejected for the SDK path, though it remains correct for §6.4: JSON Schema is the right format for data a creator publishes and we validate, and a poor fit for types a creator’s TypeScript needs at authoring time. The two boundaries have different consumers and the SDK’s is the type-checker.

Consequences (including negative)

  • The SDK gains a small runtime branch and a legibility obligation. A non-conformant value must fail registration with an error naming the interface and listing conformant libraries — otherwise the flexibility reads to a creator as “nothing works and it does not say why”.
  • Documentation cost is real and recurring. Every example must be written in one library while the contract accepts several, and it must say which is which. Examples that quietly imply a requirement are how a default becomes a de facto mandate.
  • Support surface widens. A creator can now report a bug that is their validator’s, and triage has to establish which. Accepted: the alternative is refusing them the library they know.
  • The platform loses the ability to reason about validator behaviour. Coercion, unknown-key handling and error shapes differ between conformant libraries. This is why clause 4 is load-bearing rather than a restatement: server-side re-validation is what makes the variation harmless, and removing it would make this decision unsafe rather than merely looser.
  • No bundle-size claim is made here. The registry entry created for it (Q-082) is Pending, not — the difference matters: nothing has been measured and no budget has been decided, so the row exists to record that the SDK’s client-side footprint is currently unbounded rather than to publish an estimate.

Rights-holders (ADR-079)

None. This decision retains no new class of data. A creator’s choice of validation library is a build-time dependency of their own package and is never transmitted to or stored by the platform.

Enforcement

  • The SDK’s public type accepts the Standard Schema interface and no library type. This is a compile-time property of the published .d.ts: a signature naming ZodType would make clause 1 false, and the type is the check. This is the enforcement point — the other three are supporting.
  • A test asserts registration succeeds with at least two conformant libraries and fails legibly with a non-conformant value. Two, not one, because a single-library test passes identically whether the seam exists or is hard-coded.
  • Q-082 is a registered Pending row, so the absent bundle budget is visible as undecided rather than as unbounded.
  • Server-side re-validation is asserted where it already is — the ingestion gate (ADR-098) and command admission. No new mechanism is introduced by this ADR, which is the point of the Depends-on link rather than a restatement.

Honest scope. None of the above exists yet: @descent-vtt/sdk is not built, and the frontend has no validation seam at all. This ADR settles the shape before the first consumer, which is the only moment at which it is cheap. Recorded as a decision awaiting implementation, not as an implemented one.

§11 Index Line

ADR-099: The SDK accepts any Standard Schema-conformant validator and names none in its published contract, because naming one makes every creator inherit it and makes replacement an SDK major; Valibot is the documented default in templates and examples and explicitly not a requirement; the SDK bundles no validator; creator-side validation is advisory in ADR-098's existing sense and is re-validated server-side, which is what makes accepting any library safe; keeping Zod was rejected because the defect is the naming rather than the name, mandating Valibot because it re-incurs the same defect, and an SDK-owned adapter layer because it rebuilds a seam every candidate already implements. (Depends-on ADR-098.)