Skip to content

Mark JSON Schema exports of partial codec encodings as approximate - #10161

Open
tim-smart wants to merge 5 commits into
agent/nelson-effect-bot/9b3336c8505efrom
agent/nelson/aee60a6d5f8c
Open

tim-smart wants to merge 5 commits into
agent/nelson-effect-bot/9b3336c8505efrom
agent/nelson/aee60a6d5f8c

Conversation

@tim-smart

@tim-smart tim-smart commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #10079. Retarget once that PR merges.

Problem

Schema.toJsonSchemaDocument exports only the last encoding of a codec. If a transformation on the way can reject encoded values (Date, URL, DateFromString, NumberFromString, ...), drops checks on its decoded side (Date.check(...), Number.check(isGreaterThan(0))), or uses a decoding default that fails or doesn't decode, the exported schema accepts more than the codec does. The exporter still treated it as exact, so a oneOf whose branch encodings overlap rejected values the codec accepts.

Change

  • toRepresentation marks a representation with "~approximateJson": true when it is reached through an encoding chain that isn't exact. A chain is exact when every link is total, keeps the checks of its decoded side, and every codec nested in a decoded side is exact too. The nested rule covers Option(NumberFromString) and a decoding default over NumberFromString.
  • A $ref can't carry the marker, so an approximate use of a shared definition marks the definition. That can only downgrade a union to anyOf.
  • The JSON Schema compiler treats a marked representation as approximate. Since the marking happens in the public toRepresentation, SchemaRepresentation.toJsonSchemaDocument(SchemaRepresentation.toRepresentation(...)) gives the same result as Schema.toJsonSchemaDocument.
  • "Total" is an internal allowlist: SchemaAST.total(link, predicate?) / isTotal, backed by a WeakMap. It covers:
    • the primitive JSON lowerings (numberToJson, undefinedToNull, bigint/symbol/literal links, unknownToJson, objectKeywordToJson);
    • Class constructor links;
    • Option, ReadonlyMap and ReadonlySet;
    • withDecodingDefault and withDecodingDefaultKey. These are total only when the default is a successful Exit whose value decodes through the schema it fills. That check runs lazily, on first inspection, so suspended schemas still work.
  • mapLink keeps the mark when the codec derivation re-encodes a link's target.
  • Every other link counts as partial, including every user decodeTo and passthroughs. Declarations without a JSON codec lower through a partial opaqueToJson link, so their {} export is approximate too.

Compatibility

  • None of the existing JSON Schema export, OpenAPI or AI structured-output expectations change.
  • Public representations of encoded sides that accept more than their codec gain the "~approximateJson" annotation. That updates 8 expectations in toRepresentation.test.ts / toRepresentations.test.ts. The key is excluded from code generation and JSON Schema annotations.

Tests

The regressions from the two test commits now pass. A table test checks that Class, Option and decoding defaults keep oneOf, and that nested partial codecs and dropped Number checks switch to anyOf.

Closes EFF-3104

@changeset-bot

changeset-bot Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4c3b7e8

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 33 packages
Name Type
effect Patch
@effect/opentelemetry Patch
@effect/vitest Patch
@effect/ai-anthropic Patch
@effect/ai-cloudflare Patch
@effect/ai-openai-compat Patch
@effect/ai-openai Patch
@effect/ai-openrouter Patch
@effect/ai-typesafe Patch
@effect/atom-react Patch
@effect/atom-solid Patch
@effect/atom-vue Patch
@effect/platform-browser Patch
@effect/platform-bun Patch
@effect/platform-deno Patch
@effect/platform-node-shared Patch
@effect/platform-node Patch
@effect/sql-clickhouse Patch
@effect/sql-d1 Patch
@effect/sql-libsql Patch
@effect/sql-mssql Patch
@effect/sql-mysql2 Patch
@effect/sql-pg Patch
@effect/sql-pglite Patch
@effect/sql-sqlite-bun Patch
@effect/sql-sqlite-do Patch
@effect/sql-sqlite-node Patch
@effect/sql-sqlite-react-native Patch
@effect/sql-sqlite-wasm Patch
@effect/docgen Patch
@effect/doctest Patch
@effect/graphql-generator Patch
@effect/openapi-generator Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

A JSON Schema export describes only the last encoding of a codec. When a
transformation along the way can reject encoded values, or drops checks
on its decoded side, the exported schema accepts more than the codec, so
oneOf unions must fall back to anyOf.

Links are treated as exact only when they are on an internal allowlist
of total links: the primitive JSON lowerings, Class constructors,
Option/ReadonlyMap/ReadonlySet codecs and decoding defaults.
…route

A decoding default is total only when it is a successful Exit whose value
decodes through the schema it fills. The check runs lazily, the first time
the JSON Schema export inspects the link, so suspended schemas still work.

SchemaRepresentation.toRepresentation now always records approximate
encoded sides, so the public representation route exports the same JSON
Schema as Schema.toJsonSchemaDocument.
@tim-smart
tim-smart force-pushed the agent/nelson/aee60a6d5f8c branch from 8fee1f7 to 60ed00e Compare October 29, 2026 22:34

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant