Skip to content

fix(json-schema): safer value coercion and smarter union matching#1729

Merged
dinwwwh merged 1 commit into
middleapi:mainfrom
dinwwwh:claude/json-schema-coercer-tests-226cf5
Jul 26, 2026
Merged

fix(json-schema): safer value coercion and smarter union matching#1729
dinwwwh merged 1 commit into
middleapi:mainfrom
dinwwwh:claude/json-schema-coercer-tests-226cf5

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Jul 26, 2026

Copy link
Copy Markdown
Member

Summary

Makes the smart coercion plugins safer and more predictable. Values only convert when the result is unambiguous and lossless, unions pick the branch that actually fits the payload, and malformed schemas or hostile payloads can no longer break a request.

const handler = new OpenAPIHandler(router, {
  plugins: [
    new SmartCoercionHandlerPlugin({
      converters: [new ZodToJsonSchemaConverter()],
    }),
  ],
})

// GET /messages?type=text&length=12&tracking=xyz
// the extra `tracking` key no longer stops `length` from becoming a number

What changed

  • Numbers: only plain numeric strings within the safe integer range convert. No scientific notation, no hex, and a 64 bit id such as '12345678901234567890' keeps its digits.
  • Dates: strict ISO 8601 only, now including UTC offsets (-07:00) and expanded years. Runtime dependent formats such as '12-25-2020' stay untouched.
  • BigInts: '' no longer becomes 0n, and hex or padded strings stay untouched.
  • Unions: branches are ranked. A branch that already accepts the value wins, unknown keys no longer disqualify a discriminated union branch, and a branch that describes every key beats one that leaves keys unrecognized.
  • Robustness: $ref cycles no longer overflow the stack, $ref cannot resolve into prototype members, __proto__ payload keys stay own properties, and an invalid patternProperties regex is skipped instead of throwing.
  • get in @orpc/shared treats prototype members as missing. Call sites that only index routers or contracts now do so directly.
  • Coercer tests rewritten around real-world payloads (schemas as ZodToJsonSchemaConverter emits them) with 100% coverage, including invalid schema edge cases.
  • Docs updated to match the new conversion rules.

Coerce only unambiguous, lossless values: plain numeric strings within
the safe integer range, strict ISO 8601 dates (now with UTC offsets and
expanded years), and integer strings for bigints. Rank union branches so
unknown keys no longer disqualify a discriminated union branch, stop
$ref cycles, keep __proto__ as an own property, and skip invalid
patternProperties regexes instead of throwing.

Harden the shared get helper so paths cannot reach prototype members,
and index routers/contracts directly at call sites that no longer need
it. Rewrite coercer tests around real-world payloads with full coverage,
including invalid schema edge cases.
@vercel

vercel Bot commented Jul 26, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
orpc Ready Ready Preview, Comment Jul 26, 2026 8:33am

@pkg-pr-new

pkg-pr-new Bot commented Jul 26, 2026

Copy link
Copy Markdown
More templates

@orpc/ai-sdk

npm i https://pkg.pr.new/@orpc/ai-sdk@1729

@orpc/arktype

npm i https://pkg.pr.new/@orpc/arktype@1729

@orpc/bun

npm i https://pkg.pr.new/@orpc/bun@1729

@orpc/client

npm i https://pkg.pr.new/@orpc/client@1729

@orpc/cloudflare

npm i https://pkg.pr.new/@orpc/cloudflare@1729

@orpc/contract

npm i https://pkg.pr.new/@orpc/contract@1729

@orpc/experimental-effect

npm i https://pkg.pr.new/@orpc/experimental-effect@1729

@orpc/evlog

npm i https://pkg.pr.new/@orpc/evlog@1729

@orpc/json-schema

npm i https://pkg.pr.new/@orpc/json-schema@1729

@orpc/nest

npm i https://pkg.pr.new/@orpc/nest@1729

@orpc/next

npm i https://pkg.pr.new/@orpc/next@1729

@orpc/openapi

npm i https://pkg.pr.new/@orpc/openapi@1729

@orpc/opentelemetry

npm i https://pkg.pr.new/@orpc/opentelemetry@1729

@orpc/pinia-colada

npm i https://pkg.pr.new/@orpc/pinia-colada@1729

@orpc/pino

npm i https://pkg.pr.new/@orpc/pino@1729

@orpc/publisher

npm i https://pkg.pr.new/@orpc/publisher@1729

@orpc/ratelimit

npm i https://pkg.pr.new/@orpc/ratelimit@1729

@orpc/server

npm i https://pkg.pr.new/@orpc/server@1729

@orpc/shared

npm i https://pkg.pr.new/@orpc/shared@1729

@orpc/swr

npm i https://pkg.pr.new/@orpc/swr@1729

@orpc/tanstack-query

npm i https://pkg.pr.new/@orpc/tanstack-query@1729

@orpc/trpc

npm i https://pkg.pr.new/@orpc/trpc@1729

@orpc/valibot

npm i https://pkg.pr.new/@orpc/valibot@1729

@orpc/zod

npm i https://pkg.pr.new/@orpc/zod@1729

commit: 6d9d191

@codecov

codecov Bot commented Jul 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 18 untouched benchmarks


Comparing dinwwwh:claude/json-schema-coercer-tests-226cf5 (6d9d191) with main (6941896)

Open in CodSpeed

@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 rough edge worth flagging — everything else looks solid.

Reviewed changes — tightens the JSON Schema coercer with safe-lossless conversion rules, 3-level satisfaction for union matching, $ref cycle detection, and prototype-pollution hardening. Migrates internal get() callers off prototype traversal and rewrites tests around real-world converter output shapes.

  • Safer number/bigint/date coercion — strict regex patterns reject scientific notation, hex, leading zeros, and ambiguous date formats; safe-integer boundary checks prevent digit loss on large numbers.
  • Smarter union matching with 3-level satisfactionLOOSELY_SATISFIED lets branches match when some keys are unrecognized; untouched-value preference avoids unnecessary conversions; discriminator branches tolerate extra keys.
  • $ref cycle detectionappliedRefs set tracks already-resolved schemas to prevent infinite recursion on self-referencing or circular $ref chains.
  • Prototype-pollution hardening__proto__ and constructor are treated as own properties in object coercion; $ref resolution uses Object.hasOwn to avoid prototype traversal; get() in @orpc/shared now skips inherited properties.
  • Graceful degradation — invalid patternProperties regexes are skipped instead of throwing; unresolvable $refs, unknown types, and empty unions/enums pass through unchanged.
  • Documentation updatedsmart-coercion.md reflects the new conversion rules; pointer references in server-side-client.md use direct bracket access.

ℹ️ get() in @orpc/shared is a technically breaking change

The Object.hasOwn guard now skips values inherited from prototypes. For external consumers passing plain objects this is a pure security improvement, but anyone using get() on class instances with inherited properties will see those properties suddenly resolve to undefined.

All internal call sites have been migrated to direct bracket access or audited as plain-object-only. The PR body mentions this change underneath the get bullet point but does not carry a BREAKING CHANGE: footer.

Technical details
# `get()` prototype-dependency breaking change

## Affected sites
- `packages/shared/src/object.ts:62``!Object.hasOwn(current, key)` added to the loop guard

## Required outcome
- If this is a deliberate breaking change, add `BREAKING CHANGE: get() no longer traverses prototype properties` to the commit/PR body so dependents know to audit their usage.
- If this is not intended as a breaking change, the function could be documented as "own-properties-only" with a deprecation for prototype-dependent uses.

## Suggested approach (optional)
The simplest path is to note it in the changelog. The change is well-motivated (security) and the affected surface is small (the only `get()` callers that walked non-plain-object paths were the router-utils and nest implement files, now migrated).

## Open questions for the human (optional)
- Should `@orpc/shared` bump a major version for this? The previous behavior let `get({}, ['toString'])` return `Object.prototype.toString`, which is arguably a bug.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

Comment thread packages/shared/src/object.ts

Copilot AI 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.

Pull request overview

This PR hardens JSON Schema–driven “smart coercion” across oRPC by making conversions more conservative (aiming for unambiguous/lossless results), improving union-branch selection, and adding several robustness protections against malformed schemas and hostile payloads (e.g. $ref cycles, prototype-related edge cases). It also updates a shared get() helper to ignore prototype members and adjusts a few call sites/docs accordingly.

Changes:

  • Update @orpc/shared/get to return undefined for prototype members and add tests for prototype-key paths.
  • Refine JsonSchemaCoercer union matching and schema/payload hardening (cycle detection, safer object/property handling, invalid patternProperties tolerance) with an expanded test suite.
  • Replace some internal uses of get(obj, [key]) with direct indexing in router/contract utilities and update docs to match new coercion rules.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
packages/tanstack-query/src/router-utils.ts Switches nested-client access from get() to direct indexing in recursive utils.
packages/swr/src/router-utils.ts Removes get() import and uses direct indexing for nested-client traversal.
packages/shared/src/object.ts Makes get() treat prototype members as missing via Object.hasOwn.
packages/shared/src/object.test.ts Adds coverage asserting prototype keys (__proto__, constructor, etc.) return undefined.
packages/pinia-colada/src/router-utils.ts Switches nested-client access from get() to direct indexing in recursive utils.
packages/nest/src/implement.ts Replaces get(contract,[key]) with contract[key] in decorator recursion.
packages/json-schema/src/coercer.ts Introduces satisfaction scoring for unions, adds $ref cycle protection, tightens coercion helpers, and hardens object handling.
packages/json-schema/src/coercer.test.ts Rewrites/expands tests around real converter output and edge cases.
apps/content/learn-and-contribute/mini-orpc/server-side-client.md Updates guide snippet to use direct indexing instead of get().
apps/content/docs/plugins/smart-coercion.md Updates smart coercion docs to reflect new safety/union rules and links to current sources.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/tanstack-query/src/router-utils.ts
Comment thread packages/swr/src/router-utils.ts
Comment thread packages/pinia-colada/src/router-utils.ts
Comment thread packages/nest/src/implement.ts
Comment thread packages/json-schema/src/coercer.ts
Comment thread packages/json-schema/src/coercer.ts
Comment thread packages/json-schema/src/coercer.ts
Comment thread packages/json-schema/src/coercer.ts
@dinwwwh
dinwwwh merged commit 91471ef into middleapi:main Jul 26, 2026
11 checks passed
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.

2 participants