Skip to content

Pass already valid class instances through Schema make unchanged - #6917

Open
spencerbeggs wants to merge 4 commits into
Effect-TS:mainfrom
spencerbeggs:fix/schema-class-array-identity-fast-path
Open

Pass already valid class instances through Schema make unchanged#6917
spencerbeggs wants to merge 4 commits into
Effect-TS:mainfrom
spencerbeggs:fix/schema-class-array-identity-fast-path

Conversation

@spencerbeggs

@spencerbeggs spencerbeggs commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Closes #6890.

toConstructorAST rewrites every class Declaration into its constructor form unconditionally, so a field typed Schema.Array(MyClass) rebuilds each element on make even when the element is already an instance of exactly that class. The result is a fresh tree and the loss of the caller's references.

This registers the constructor-form ASTs it produces, and makeParser consults that set: when the input already satisfies the declaration and its checks, it is returned unchanged and the encoding chain is skipped. Only the constructor direction is affected, since toConstructorAST is reached solely from makeEffect; decoding and encoding are untouched.

The failure from that attempt is deliberately discarded rather than reported. The encoding chain below still runs and produces the authoritative issue, so error messages are byte-identical to before. That detail matters: a first attempt at this modelled the fast path as Union([declaration, constructorForm]), which worked and was faster, but made every failed make on a class field carry a spurious Expected B, got {...} prefix from the union's other branch. That approach is not in this PR.

Measured

Table.make({ rows }) over 30,000 pre-built Row instances, median of 15 runs after warmup, Node 26.5.0:

before after
Schema.Array(Row) 9.27ms, identity lost 0.74ms, identity preserved

For reference the issue's Schema.Array(Schema.Union([Row])) workaround measures 1.84ms, so the class spelling is now comfortably ahead of it. (These numbers are measured on this branch after merging #6912; an earlier revision of this description quoted 8.69ms/1.51ms from the pre-merge base.)

On the issue's second suggestion

#6890 offered an alternative: document the re-construction as the class contract and treat the union spelling as the sanctioned identity-preserving form. That does not hold up. The union spelling is not an equivalent schema in the constructor direction — it rejects plain-object input outright and silently skips constructor defaults inside its members:

class  Array(Row).make({v:"a"})           OK   -> {"v":"a","tag":"DEFAULTED"}
union  Array(Union([Row])).make({v:"a"})  FAIL -> Expected Row, got {"v":"a"}

The cause is that toConstructorAST has no case "Union", so unions fall through to default: return ast and never receive constructor semantics at all. Its identity preservation is a side effect of that gap. I have left that behavior alone here — it looks like a separate bug, and I did not want to widen this PR into it — but it is worth a maintainer's eye.

Also worth correcting from the issue: the reported 126x was a cold single-shot measurement on beta.101. On main the steady-state gap is about 5x.

Validation

pnpm lint-fix, pnpm check, and vitest run --project effect — 244 files, 7658 passed, 3 skipped, including the full schema suite.

On the two new tests: disabling the guard makes make passes already valid class instances through unchanged fail with Values have same structure but are not reference-equal, so that one discriminates this change directly. The second test does not — it still passes with the guard off, because guard-off is simply the old behavior. It is a regression guard aimed at the Union shape described above, which is the mutant it actually catches.

Behavior change to weigh

make no longer returns defensive copies of class inputs. Anyone relying on the previous re-construction to isolate caller-held references will now observe the originals. That is the point of the change, but it is a visible semantic shift and belongs in a maintainer's judgement rather than mine — happy to gate it behind an option instead if you would prefer.

Signed-off-by: C. Spencer Beggs spencer@beggs.codes

toConstructorAST rewrote every class Declaration into its constructor form unconditionally, so a field such as Schema.Array(MyClass) rebuilt each element even when it was already an instance of exactly that class, allocating a fresh tree and discarding the caller's references.

Constructor-form ASTs are now tracked so makeParser can run the declaration and its checks directly against such an input and skip the encoding chain. A failure from that attempt is discarded rather than reported, so the encoding chain still produces the authoritative issue and error messages are unchanged. Only the constructor direction is affected; decoding and encoding are untouched.

Closes Effect-TS#6890
Signed-off-by: C. Spencer Beggs <spencer@beggs.codes>
@github-project-automation github-project-automation Bot moved this to Discussion Ongoing in PR Backlog Aug 3, 2026
@changeset-bot

changeset-bot Bot commented Aug 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: f33e3db

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

This PR includes changesets to release 30 packages
Name Type
effect Patch
@effect/opentelemetry Patch
@effect/platform-browser Patch
@effect/platform-bun Patch
@effect/platform-deno Patch
@effect/platform-node-shared Patch
@effect/platform-node Patch
@effect/vitest Patch
@effect/ai-anthropic Patch
@effect/ai-openai-compat Patch
@effect/ai-openai Patch
@effect/ai-openrouter Patch
@effect/atom-react Patch
@effect/atom-solid Patch
@effect/atom-vue 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

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ One minor suggestion — otherwise looks great.

Reviewed changes — a single commit that skips the encoding chain in Schema.make when a class-typed input already satisfies its declaration and checks, preserving identity and avoiding redundant re-construction.

  • SchemaParser fast-path guard: toConstructorAST registers constructor-form class ASTs in a WeakSet; makeParser speculatively calls parseLocal and returns the input unchanged when it passes, falling through to the encoding chain on failure so error messages stay identical.
  • Tests: identity preservation for Schema.Array(MyClass), constructor-default application for plain objects, and check-failure messages on nested class fields (check + makeFilter).

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) (Kimi K2 not used — the program covers this model; add its provider key to run your pick) | 𝕏

Comment thread .changeset/schema-class-constructor-identity.md

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — one merge commit (96444c81f) brings the branch up to date with main; the PR's own content (3 files: SchemaParser.ts, Schema.test.ts, .changeset/) is unchanged since the prior review at 6ca694e44. Prior feedback (non-blocking changeset style suggestion) was resolved.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) (Kimi K2 not used — the program covers this model; add its provider key to run your pick) | 𝕏

…class make test

The test was written against a base that predated the removal of actual values from schema issues, so it expected the old "Expected positive n, got {...}" rendering. The branch now carries that change and the assertion matches it.

Signed-off-by: C. Spencer Beggs <spencer@beggs.codes>

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — one commit (9225e3b) drops the , got {"n":0} suffix from a makeFilter failure message assertion to match the error format from #6912 and the other makeFilter tests in the file.

  • Fixed test assertion format: the new class-make test at Schema.test.ts:6495 previously asserted an error message that included a , got {…} suffix; the filter issue format doesn't produce that suffix, so the assertion was wrong. Aligned with the format used by lines 6929–6930.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) (Kimi K2 not used — the program covers this model; add its provider key to run your pick) | 𝕏

@github-actions

github-actions Bot commented Aug 3, 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
basic.ts 7.06 KB 7.06 KB 0.00 KB (0.00%)
batching.ts 9.86 KB 9.86 KB 0.00 KB (0.00%)
brand.ts 6.34 KB 6.34 KB 0.00 KB (0.00%)
cache.ts 10.62 KB 10.62 KB 0.00 KB (0.00%)
config.ts 20.64 KB 20.60 KB +0.03 KB (+0.16%)
differ.ts 20.24 KB 20.20 KB +0.04 KB (+0.19%)
http-client.ts 21.49 KB 21.49 KB 0.00 KB (0.00%)
logger.ts 10.76 KB 10.76 KB 0.00 KB (0.00%)
metric.ts 8.99 KB 8.99 KB 0.00 KB (0.00%)
optic.ts 7.18 KB 7.18 KB 0.00 KB (0.00%)
pubsub.ts 14.90 KB 14.90 KB 0.00 KB (0.00%)
queue.ts 11.58 KB 11.58 KB 0.00 KB (0.00%)
schedule.ts 10.74 KB 10.74 KB 0.00 KB (0.00%)
schema-class.ts 19.19 KB 19.14 KB +0.04 KB (+0.23%)
schema-fromJsonSchemaDocument.ts 29.01 KB 28.96 KB +0.06 KB (+0.19%)
schema-representation-roundtrip.ts 25.33 KB 25.29 KB +0.05 KB (+0.19%)
schema-string-transformation.ts 13.33 KB 13.30 KB +0.03 KB (+0.25%)
schema-string.ts 10.98 KB 10.94 KB +0.04 KB (+0.34%)
schema-template-literal.ts 15.20 KB 15.17 KB +0.03 KB (+0.20%)
schema-toArbitraryLazy.ts 21.98 KB 21.94 KB +0.04 KB (+0.20%)
schema-toCodeDocument.ts 24.38 KB 24.34 KB +0.04 KB (+0.16%)
schema-toCodecJson.ts 19.22 KB 19.18 KB +0.05 KB (+0.24%)
schema-toEquivalence.ts 19.05 KB 19.01 KB +0.04 KB (+0.21%)
schema-toFormatter.ts 18.90 KB 18.87 KB +0.04 KB (+0.19%)
schema-toJsonSchemaDocument.ts 22.64 KB 22.60 KB +0.05 KB (+0.20%)
schema-toRepresentation.ts 19.56 KB 19.52 KB +0.04 KB (+0.21%)
schema.ts 18.45 KB 18.41 KB +0.04 KB (+0.22%)
stm.ts 12.54 KB 12.54 KB 0.00 KB (0.00%)
stream.ts 9.79 KB 9.79 KB 0.00 KB (0.00%)

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

Labels

4.0 bug Something isn't working

Projects

Status: Discussion Ongoing

1 participant