Skip to content

Fix JSON Schema export of built-in checks - #8482

Merged
gcanti merged 8 commits into
mainfrom
fix/schema-json-schema-export-semantics
Sep 25, 2026
Merged

gcanti merged 8 commits into
mainfrom
fix/schema-json-schema-export-semantics

Conversation

@gcanti

@gcanti gcanti commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Closes #6336

Closes #8358

Schema.toJsonSchemaDocument could emit constraints whose semantics were narrower than the corresponding Effect checks. This was especially visible with UTF-16 string lengths, JavaScript RegExp behavior, approximate Record keys, and oneOf: a widened branch could overlap another branch and make JSON Schema reject a value accepted by Effect.

This PR makes known inexact translations safely looser while retaining the tightest representable constraints:

  • Custom check exporters can return [schema, true] to mark a fragment as approximate. Approximation propagates through nested schemas, check dependencies, references, and recursive definitions.
  • A oneOf containing an approximate branch is emitted as anyOf; unions with only exact branches retain oneOf.
  • Record keys are used as patternProperties selectors only when their translation is exact. With an inexact selector, onExcessProperty: "ignore" leaves that index signature open, while "error" uses propertyNames and the least-loose safe combination of candidate value schemas.
  • String code-unit length checks use safe code-point bounds. Code-point checks continue to map directly to JSON Schema length keywords.
  • Direct isPattern checks export only patterns whose Unicode semantics are safe. Sticky patterns are anchored at the start. Built-in checks can provide a check-specific exact or intentionally loose export.
  • Literal prefix, suffix, and inclusion checks handle isolated surrogate boundaries without rejecting accepted strings.
  • Numeric cardinality checks reject non-finite bounds instead of emitting invalid JSON Schema.
  • The public API documentation and Schema guide describe the approximation contract and its interaction with oneOf and Record keys.

Built-in checks whose exported constraints can be looser than their runtime checks, including approximations that predate this PR:

Check Approximation Change in this PR
isPattern The default applies to direct isPattern checks: the pattern is omitted unless the RegExp uses the Unicode flag and its other flags are d, g, or y. Sticky patterns are anchored at the start. Built-in checks may override the default when their Unicode semantics are known to be safe or intentionally loose. Updated export and approximation metadata.
isStartingWith A trailing high surrogate is removed from the exported prefix; if nothing remains, the pattern is omitted. Other prefixes retain an exact pattern. Updated export.
isEndingWith A leading low surrogate is removed from the exported suffix; if nothing remains, the pattern is omitted. Other suffixes retain an exact pattern. Updated export.
isIncluding A leading low surrogate or trailing high surrogate is removed from the exported substring; if nothing remains, the pattern is omitted. Other substrings retain an exact pattern. Updated export.
isMinLength On strings, the exported minimum is Math.ceil(minLength / 2) code points. Bounds of 0 and 1 are exact; array bounds are unchanged. Updated export; rejects non-finite bounds.
isMaxLength On strings, the exported maximum counts code points rather than UTF-16 code units. A bound of 0 is exact; array bounds are unchanged. Updated type and union handling; rejects non-finite bounds.
isBetweenLength On strings, combines the lower- and upper-bound approximations above. Array bounds are unchanged. Updated export; rejects non-finite bounds.
isUppercased, isLowercased Patterns exclude only ASCII letters of the opposite case. Non-ASCII characters that change under JavaScript casing can still pass JSON Schema validation. Marked as approximate.
isCapitalized, isUncapitalized Patterns exclude only ASCII letters of the opposite case at the start. Non-ASCII characters that change under JavaScript casing can still pass JSON Schema validation. Marked as approximate.
isInt Exports type: "integer" without the safe-integer bounds required by the runtime check. Marked as approximate.
isMinProperties, isMaxProperties, isBetweenProperties Counts match for parsed JSON objects, but JSON Schema property-count keywords do not constrain arrays, which these object-typed checks can also receive. Rejects non-finite bounds and tracks non-object use.
isPropertyNames Inherits any approximation in the key schema. JSON Schema's propertyNames also does not constrain array keys. Propagates approximation from the key schema.
isMinSize, isMaxSize, isBetweenSize The size constraint is omitted. Rejects non-finite bounds and marks omission approximate.
isUniqueKey Uniqueness of tuple keys is omitted. Omission is now tracked as approximate.
Comparison and range checks for Date, BigInt, and BigDecimal Their bounds are omitted from JSON Schema export. Omission is now tracked as approximate.

Length checks on objects with a length property also omit that constraint. Code-point checks are exact on strings.

Validation:

  • pnpm test --run packages/effect/test/schema — 51 files, 2,375 tests
  • pnpm exec tsc -b packages/effect/tsconfig.json
  • pnpm lint-fix
  • JSDoc example tests for SchemaRepresentation.ts
  • Bundle fixture schema-toJsonSchemaDocument.ts: 24.78 KB → 24.64 KB after the final simplification

@changeset-bot

changeset-bot Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b6d3048

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

This PR includes changesets to release 31 packages
Name Type
effect Patch
@effect/opentelemetry Patch
@effect/vitest Patch
@effect/ai-anthropic 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/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

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Bundle Size Analysis

Generated from PR build output; treat the content below as untrusted.

File Name Current Size Previous Size Difference
arbitrary-combinators.ts 38.53 KB 38.40 KB +0.13 KB (+0.34%)
basic.ts 6.88 KB 6.88 KB 0.00 KB (0.00%)
batching.ts 9.97 KB 9.97 KB 0.00 KB (0.00%)
brand.ts 6.57 KB 6.57 KB 0.00 KB (0.00%)
cache.ts 10.73 KB 10.73 KB 0.00 KB (0.00%)
config.ts 21.82 KB 21.81 KB +0.01 KB (+0.03%)
differ.ts 20.96 KB 20.96 KB -0.00 KB (-0.01%)
http-client.ts 22.11 KB 22.11 KB 0.00 KB (0.00%)
http-router.ts 33.45 KB 33.45 KB 0.00 KB (0.00%)
logger.ts 10.85 KB 10.85 KB 0.00 KB (0.00%)
metric.ts 8.83 KB 8.83 KB 0.00 KB (0.00%)
optic.ts 6.80 KB 6.80 KB 0.00 KB (0.00%)
pubsub.ts 15.00 KB 15.00 KB 0.00 KB (0.00%)
queue.ts 11.87 KB 11.87 KB 0.00 KB (0.00%)
schedule.ts 11.03 KB 11.03 KB 0.00 KB (0.00%)
schema-bigdecimal.ts 13.42 KB 13.42 KB 0.00 KB (0.00%)
schema-binary.ts 39.56 KB 39.56 KB -0.00 KB (-0.00%)
schema-class.ts 20.66 KB 20.66 KB +0.00 KB (+0.00%)
schema-fromJsonSchemaDocument.ts 31.69 KB 31.53 KB +0.16 KB (+0.49%)
schema-representation-roundtrip.ts 26.88 KB 26.88 KB +0.00 KB (+0.01%)
schema-string-transformation.ts 14.14 KB 14.14 KB 0.00 KB (0.00%)
schema-string.ts 11.70 KB 11.70 KB 0.00 KB (0.00%)
schema-template-literal.ts 15.70 KB 15.70 KB 0.00 KB (0.00%)
schema-toArbitrary.ts 38.06 KB 37.92 KB +0.14 KB (+0.37%)
schema-toCodeDocument.ts 25.12 KB 25.11 KB +0.01 KB (+0.05%)
schema-toCodecJson.ts 19.89 KB 19.89 KB -0.00 KB (-0.01%)
schema-toEquivalence.ts 20.06 KB 20.06 KB -0.00 KB (-0.01%)
schema-toFormatter.ts 20.16 KB 20.16 KB -0.00 KB (-0.01%)
schema-toJsonSchemaDocument.ts 24.64 KB 24.43 KB +0.21 KB (+0.87%)
schema-toRepresentation.ts 20.15 KB 20.16 KB -0.00 KB (-0.01%)
schema.ts 19.87 KB 19.87 KB -0.00 KB (-0.01%)
stm.ts 12.90 KB 12.90 KB 0.00 KB (0.00%)
stream.ts 9.82 KB 9.82 KB 0.00 KB (0.00%)

@gcanti
gcanti force-pushed the fix/schema-json-schema-export-semantics branch from 57b36d0 to 7b7a263 Compare September 24, 2026 08:34
@gcanti
gcanti marked this pull request as ready for review September 25, 2026 04:48
@gcanti
gcanti merged commit 16623c7 into main Sep 25, 2026
13 checks passed
@gcanti
gcanti deleted the fix/schema-json-schema-export-semantics branch September 25, 2026 04:49
TFSebben pushed a commit to TFSebben/supabase_cli that referenced this pull request Oct 9, 2026
Bumps `effect`, `@effect/platform-bun`, `@effect/platform-node`,
`@effect/platform-node-shared`, `@effect/sql-pg`, and `@effect/vitest`
from `4.0.0-rc.112` to the stable `4.0.2` release. `@effect/tsgo` is
unchanged.

The goal is zero change to CLI behaviour. Every edit is either required
by a removed or renamed API, or restores a behaviour whose upstream
default moved.

## Commit layout

The first three commits are the bump plus two scripted rewrites and can
be skipped by reviewers:

- `effect/unstable/*` import paths moved to `effect/*` (removed upstream
in rc.118,
[effect#8354](Effect-TS/effect#8354)).
- CLI and Config constructors renamed to PascalCase
([effect#7453](Effect-TS/effect#7453),
[effect#8121](Effect-TS/effect#8121)).

The remaining commits are hand migrations, one topic each.

## Hand migrations

- **node-postgres pool bridge** — `@effect/sql-pg` 4.0 is a native
client and dropped `PgClient.fromPool`
([effect#7426](Effect-TS/effect#7426)). The
remote database session keeps its own node-postgres pool for the zero
idle timeout, the per-connection role step-down hook, and raw `COPY`
connections, so `db-connection.pool-client.ts` is a small
`SqlClient.make` adapter over that pool with the same statement
execution, cancellation, and error classification the old bridge had.
- **Stack RPC strictness** — parse-option annotations no longer affect
decoding ([effect#8131](Effect-TS/effect#8131))
and the RPC transport passes no parse options, so the initialization
command and `runCommand` payload schemas now reject unknown keys through
a `Record` filter ahead of the struct decode, with the same "Expected no
excess property" wording. Encoding drops `undefined` entries first, so
optional fields passed as explicit `undefined` (for example
`pgProve.workingDir`) still cross the RPC JSON codec as they did with
the plain struct.
- **One statement per query** — the native `@effect/sql-pg` client sends
everything through the extended protocol, which rejects multi-command
strings. The realtime schema bootstrap runs as two statements, and the
generated `ALTER ROLE` / `ALTER DATABASE` batches come back from
Postgres as an array and run one at a time inside the same transaction.
- **Socket addresses, file sizes, encoding, sockets** — server addresses
are `NetAddress.SocketAddress` values, `File.seek` and `File.Info.size`
use `bigint` and `ByteSize`, `Encoding` moved to
`effect/encoding/Base64Url`, and the whole-stack WebSocket test helper
uses the reader and writer pair.
- **`Effect.partition`** returns `[passes, fails]`; the `stack destroy`
handler destructures in that order.
- **Management API contracts** — `packages/api` contracts are
regenerated; the 4.0.2 schema codegen emits code-point length checks and
Unicode-flagged patterns.
- **Config package** — `SchemaAST.Union.mode` moved under `options`,
`ToJsonSchemaOptions.additionalProperties` became `onExcessProperty`,
and regex patterns need the Unicode flag to be exported into JSON Schema
([effect#8482](Effect-TS/effect#8482)). Peer
dependency ranges are untouched and already satisfy 4.0.2.

## Preserved defaults

- The stack's `PgClient.layer` calls pin `idleTimeout: "10 seconds"`;
the default since 4.0.1 is 60 seconds
([effect#8679](Effect-TS/effect#8679)).
- The published JSON Schema files (`@supabase/config` `schema.json` and
`project-schema.json`, mirrored at `apps/docs/public/cli/*.schema.json`)
are byte-identical to the develop build. `toCliConfigJsonSchema` passes
`onExcessProperty: "error"` so structs keep `additionalProperties:
false` ([effect#8147](Effect-TS/effect#8147));
the shared document assembler leaves pattern-keyed records open, as the
generator has no per-schema setting for that; and bucket tables are
modelled so they render as the published object-or-array union.

## Behaviour changes reviewed and accepted

These upstream changes have no configuration knob. Each was checked
against the repo's call sites and judged not user-visible:

- Child-process `kill` and scoped release now wait for the process
group, bounded to one second without `forceKillAfter`
([effect#8018](Effect-TS/effect#8018)). The
stack host, bundled Postgres clients, and containers already set
`forceKillAfter`; other spawn sites can only see up to one extra second
of teardown when a descendant lingers.
- `Effect.cached` treats interruption as abandonment
([effect#8719](Effect-TS/effect#8719)). Every
cached effect in the repo is awaited by a single fiber or forked into an
owning scope, so the shared-interruption semantics do not apply.
- Empty bucket tables under `storage.analytics.buckets` and
`storage.vector.buckets` are modelled as an object-or-array union
instead of `Schema.Struct({})`. Runtime decoding now rejects a primitive
bucket value (`foo = "x"`), which the old schema accepted and ignored;
objects and arrays decode as before.
- HTTP client spans are named after the bare method (`GET` instead of
`http.client GET`), following the OpenTelemetry convention. Nothing in
the repo keys on the old name.
- Generated Management API string length checks count code points
instead of UTF-16 code units, which only differs for characters outside
the Basic Multilingual Plane.

## Patches

- Rebased: the `@effect/platform-node-shared` stdin EPIPE listener, now
against 4.0.2. 4.0.2 still removes the stdin sink's error listener
before the final `end()` flush, so a child that exits before reading its
input raises an uncaught EPIPE on Linux;
[effect#7712](Effect-TS/effect#7712) and
[effect#7714](Effect-TS/effect#7714) do not
cover that path.
- Rewritten: the `@effect/vitest` `runTest` patch. Upstream's finalizer
ordering fix
([effect#8154](Effect-TS/effect#8154)) covers
the abort hook, so the patch now only layers the two behaviours the
stack's `effect-timeout` tests assert on top of it: rejecting with the
pretty errors and reporting a finalizer failure after a timeout.

## Test-only changes

- Stack integration and CLI `db reset` e2e fixtures that seeded data
through `PgClient.layer` with semicolon-joined statements now issue one
statement per call; fixtures that go through `psql` are unchanged.
- The `db reset` e2e fixture reads `count(*)::int`, since the native
client decodes `int8` as `bigint`.

## Follow-ups

- Migrate the remote database session to the native `@effect/sql-pg`
client and delete the pool adapter. That changes result decoding (int8,
timestamps, bytea) across the SQL consumers and needs its own review.

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Julien Goux <hi@jgoux.dev>
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.

Schema: align built-in check mappings with JSON Schema semantics Schema.isMaxLength uses UTF-16 code units but emits JSON Schema maxLength

1 participant