Skip to content

feat(api): use camelCase for TypeScript SDK#4622

Open
tothandras wants to merge 4 commits into
mainfrom
feat/camel-case-sdk
Open

feat(api): use camelCase for TypeScript SDK#4622
tothandras wants to merge 4 commits into
mainfrom
feat/camel-case-sdk

Conversation

@tothandras

@tothandras tothandras commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features
    • SDK public request/response shapes are now camelCase, automatically translated to snake_case on the wire.
    • Added opt-in runtime payload validation (SDKOptions.validate) with validation failures returned as Result.error (via ValidationError).
    • Exposed additional wire/runtime errors (including DepthLimitExceededError).
  • Bug Fixes
    • Standardized endpoint request/response handling to use schema-based wire mapping, including dropping unknown fields.
    • Improved query/sort serialization and added explicit missing path-parameter errors for ID-based routes.
  • Documentation
    • Updated examples to use camelCase request fields and clarified sort input behavior.
  • Tests
    • Expanded wire-mapping, casing, and validation coverage; added SDK test/coverage scripts.

@tothandras tothandras requested a review from a team as a code owner July 1, 2026 07:43
@tothandras tothandras added the release-note/feature Release note: Exciting New Features label Jul 1, 2026
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9159dcff-5bd1-47fa-94c9-fc3d594c0ffa

📥 Commits

Reviewing files that changed from the base of the PR and between 3dc5afd and 2067210.

📒 Files selected for processing (20)
  • api/spec/AGENTS.md
  • api/spec/packages/aip-client-javascript/src/funcs/addons.ts
  • api/spec/packages/aip-client-javascript/src/funcs/apps.ts
  • api/spec/packages/aip-client-javascript/src/funcs/billing.ts
  • api/spec/packages/aip-client-javascript/src/funcs/currencies.ts
  • api/spec/packages/aip-client-javascript/src/funcs/customers.ts
  • api/spec/packages/aip-client-javascript/src/funcs/events.ts
  • api/spec/packages/aip-client-javascript/src/funcs/features.ts
  • api/spec/packages/aip-client-javascript/src/funcs/governance.ts
  • api/spec/packages/aip-client-javascript/src/funcs/llmCost.ts
  • api/spec/packages/aip-client-javascript/src/funcs/meters.ts
  • api/spec/packages/aip-client-javascript/src/funcs/planAddons.ts
  • api/spec/packages/aip-client-javascript/src/funcs/plans.ts
  • api/spec/packages/aip-client-javascript/src/funcs/subscriptions.ts
  • api/spec/packages/aip-client-javascript/src/funcs/tax.ts
  • api/spec/packages/aip-client-javascript/src/lib/config.ts
  • api/spec/packages/aip-client-javascript/tests/wire-helpers.ts
  • api/spec/packages/aip-client-javascript/tests/wire.spec.ts
  • api/spec/packages/typespec-typescript/src/runtime-templates.ts
  • api/spec/packages/typespec-typescript/src/sdk-files.ts
✅ Files skipped from review due to trivial changes (2)
  • api/spec/AGENTS.md
  • api/spec/packages/typespec-typescript/src/runtime-templates.ts
🚧 Files skipped from review as they are similar to previous changes (18)
  • api/spec/packages/aip-client-javascript/src/lib/config.ts
  • api/spec/packages/aip-client-javascript/tests/wire-helpers.ts
  • api/spec/packages/aip-client-javascript/src/funcs/governance.ts
  • api/spec/packages/aip-client-javascript/src/funcs/events.ts
  • api/spec/packages/aip-client-javascript/src/funcs/apps.ts
  • api/spec/packages/aip-client-javascript/src/funcs/planAddons.ts
  • api/spec/packages/aip-client-javascript/src/funcs/addons.ts
  • api/spec/packages/aip-client-javascript/src/funcs/subscriptions.ts
  • api/spec/packages/aip-client-javascript/src/funcs/currencies.ts
  • api/spec/packages/aip-client-javascript/src/funcs/tax.ts
  • api/spec/packages/aip-client-javascript/src/funcs/meters.ts
  • api/spec/packages/aip-client-javascript/src/funcs/llmCost.ts
  • api/spec/packages/aip-client-javascript/src/funcs/plans.ts
  • api/spec/packages/aip-client-javascript/tests/wire.spec.ts
  • api/spec/packages/aip-client-javascript/src/funcs/billing.ts
  • api/spec/packages/typespec-typescript/src/sdk-files.ts
  • api/spec/packages/aip-client-javascript/src/funcs/features.ts
  • api/spec/packages/aip-client-javascript/src/funcs/customers.ts

📝 Walkthrough

Walkthrough

This PR adds a camelCase SDK surface mapped to snake_case wire payloads, with build-time casing checks, runtime wire conversion/validation, regenerated SDK request/response handling, and matching docs, tests, and tooling updates.

Changes

Wire mapping boundary feature

Layer / File(s) Summary
Docs and examples
api/spec/AGENTS.md, api/spec/packages/aip-client-javascript/README.md, api/spec/packages/typespec-typescript/src/readme.ts
Documents the camelCase public surface, snake_case wire translation, and updates example request payloads to camelCase field names.
Casing helpers and build gate
.../src/casing.ts, .../src/casing-gate.ts, .../test/casing.test.ts
Adds casing conversion helpers, the casing-derivability gate, and unit tests for the new casing rules.
Emitter and schema wire-mode wiring
.../src/utils.tsx, .../src/components/ZodSchema.tsx, .../ZodSchemaDeclaration.tsx, .../zodBaseSchema.tsx, .../ZodOperations.tsx, .../interface-types.ts, .../ts-types.ts, .../request-types.ts, .../emitter.tsx
Adds wire-mode helpers, refkey selection, wire/camel schema emission, and the generator plumbing that passes body overrides and emits the wire runtime bundle.
Runtime wire mapper and client-side support
.../src/runtime/wire.ts, .../src/wire-runtime.ts, .../src/lib/wire.ts, .../src/lib/config.ts, .../src/lib/encodings.ts, .../src/index.ts
Implements the shared wire walker, validation error types, generated runtime loader, and client-side config/encoding/index exports that support the new mapping behavior.
Generated SDK templates and regenerated API funcs
.../src/sdk-files.ts, .../src/runtime-templates.ts, api/spec/package.json, .../src/funcs/*.ts, .../src/models/operations/tax.ts
Reworks the SDK templates and all generated API functions to use wire encoding, optional validation, explicit path encoding, and wire-response decoding.
Tests, coverage, and package tooling
.../tests/*, .../vitest.config.ts, .gitignore, .npmignore, api/spec/package.json
Adds wire-mapping tests, generated round-trip coverage, Vitest coverage config, and package ignore/script updates.

Estimated code review effort: 4 (Complex) | ~75 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant funcs
  participant toWire
  participant assertValid
  participant HTTP
  participant fromWire
  Caller->>funcs: call createMeter(req)
  funcs->>toWire: convert camelCase body to snake_case
  funcs->>assertValid: validate wire body (if options.validate)
  funcs->>HTTP: POST wire body
  HTTP-->>funcs: JSON response
  funcs->>assertValid: validate response wire (if options.validate)
  funcs->>fromWire: convert response to camelCase
  funcs-->>Caller: typed camelCase result
Loading

Possibly related PRs

  • openmeterio/openmeter#4501: Same meters.queryMeterCsv/wire-encoding area and closely related generated SDK request-body handling.

Suggested labels: release-note/feature

Suggested reviewers: solkimicreb, borbelyr-kong

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: switching the TypeScript SDK public surface to camelCase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/camel-case-sdk

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR changes the TypeScript SDK to expose camelCase shapes while keeping snake_case on the wire. The main changes are:

  • Schema-driven request and response casing conversion.
  • Optional wire-payload validation through generated wire schemas.
  • Updated generated operation functions for path encoding, query mapping, and response mapping.
  • Expanded SDK tests and documentation for casing and validation behavior.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
api/spec/packages/typespec-typescript/src/runtime/wire.ts Adds the runtime casing mapper used by generated SDK requests and responses.
api/spec/packages/typespec-typescript/src/sdk-files.ts Updates generated operation functions to use inline path checks, wire mapping, and validation hooks.
api/spec/packages/aip-client-javascript/src/lib/wire.ts Contains the generated SDK copy of the wire/public casing mapper.

Reviews (4): Last reviewed commit: "chore: review comments" | Re-trigger Greptile

Comment thread api/spec/packages/typespec-typescript/src/runtime/wire.ts
Comment thread api/spec/packages/typespec-typescript/src/sdk-files.ts
Comment thread api/spec/packages/typespec-typescript/src/sdk-files.ts

@coderabbitai coderabbitai 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.

Actionable comments posted: 11

🧹 Nitpick comments (1)
api/spec/packages/aip-client-javascript/.npmignore (1)

14-19: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Also ignore tests/ in the npm package.

Nice catch adding the Vitest config and coverage excludes. The new tests/ tree from this PR is still publishable, though, so consumers will get dev-only specs/helpers unless that folder is ignored too.

Suggested tweak
 vitest.config.ts
 .gitignore
+tests/
 
 # Test coverage output
 coverage/
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/spec/packages/aip-client-javascript/.npmignore` around lines 14 - 19, The
npm package ignore list is missing the new tests tree, so dev-only specs/helpers
would still be published. Update the .npmignore entry set alongside
vitest.config.ts and coverage/ to also exclude tests/, keeping the package
publishable only with runtime files.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/spec/AGENTS.md`:
- Around line 200-213: The Markdown in this contract section is broken because
the asterisks in snake*case and \_inside* are being interpreted as emphasis
instead of literal text. Update the AGENTS.md prose near SDKOptions.validate and
request() to use proper inline code or plain Markdown emphasis for the intended
terms, and verify the surrounding wording still reads correctly after replacing
those malformed fragments.

In `@api/spec/packages/aip-client-javascript/src/funcs/apps.ts`:
- Around line 31-35: The listApps response mapping in apps.ts is dropping
server-added fields because fromWire() only keeps schema-defined keys. Update
the listApps transformation to preserve unknown response properties while still
camelizing and validating the known ones via schemas.listAppsResponseWire and
schemas.listAppsResponse, so additive fields remain available to consumers when
client._options.validate is off.

In `@api/spec/packages/aip-client-javascript/src/funcs/plans.ts`:
- Around line 29-38: The listPlans query path is serializing the wire object
without runtime validation, so invalid page/sort/filter values can slip through
when client._options.validate is enabled. Update the listPlans request
construction to validate the wire query object against
schemas.listPlansQueryParams before calling toURLSearchParams, using the same
validation flow used for request bodies so bad inputs fail fast. Use the
existing listPlans query builder, toWire, encodeSort, and
schemas.listPlansQueryParams symbols to place the check in the right spot.

In `@api/spec/packages/aip-client-javascript/src/lib/config.ts`:
- Around line 22-27: The JSDoc for the validation setting currently says
failures “reject,” but the SDK actually surfaces them as a failed Result via
assertValid(...) inside request(() => ...), so callers won’t hit catch handlers.
Update the wording in the config documentation and the emitting template to
describe that validation failures return Result.error/failed Result instead of
rejecting, and keep the description aligned with the assertValid and request
flow.

In `@api/spec/packages/aip-client-javascript/src/lib/wire.ts`:
- Around line 20-22: The helper def currently reads the wrong Zod internals, so
it never returns the schema definition for Zod 4 types. Update def in wire.ts to
read from schema._zod.def instead of schema.def, and keep the rest of the walk()
logic unchanged so object, union, and record payload key renaming paths are
reached.

In `@api/spec/packages/aip-client-javascript/tests/wire-helpers.ts`:
- Around line 115-121: The key collection logic in collectFieldKeys is skipping
the entire subtree for user_key_* entries, which causes nested schema fields to
be missed. Update the loop over Object.entries(value) so preserved user record
keys are not added as schema fields themselves, but their nested values are
still traversed for leak-checking; keep the existing handling in
collectFieldKeys and any caller that relies on the generated keys list.

In `@api/spec/packages/typespec-typescript/src/casing-gate.ts`:
- Around line 68-70: The casing gate is only checking namespace-owned unions
because it iterates userUnions(program), so inline request/response unions are
skipped. Update the union scan in mappedReachableUnions/casing-gate logic to
iterate mappedUnions instead, and keep the existing ambiguous-union and
discriminator/envelope casing checks applied to every mapped union.

In `@api/spec/packages/typespec-typescript/src/emitter.tsx`:
- Around line 102-105: The current casing check in assertCasingDerivable only
verifies that each wire key can be deterministically recovered, but it does not
catch collisions where two different wire names map to the same camelCase public
name. Extend the gate around assertCasingDerivable in emitter.tsx to also detect
duplicate camelized names before emission, and fail the build when a model’s
fields or a query object’s parameters would collapse to the same public key. Use
the existing model and operation traversal to compare the derived public names
for each group and reject any duplicates before the public emitters run.

In `@api/spec/packages/typespec-typescript/src/runtime/wire.ts`:
- Around line 20-22: The literal discriminator lookup is still reading the old
Zod shape, so discriminated union renaming won’t work. Update the discriminator
extraction in the runtime wire helpers by adjusting the `literalValue()` logic
to read from `def.values` via the existing `def()` helper instead of
`schema.value`, keeping the change aligned with `ZodDef` and the `def()`
function.

In `@api/spec/packages/typespec-typescript/src/sdk-files.ts`:
- Around line 6-17: The path templating in pathExpr() no longer preserves the
fail-fast missing-parameter guard from encodePath, so a missing runtime path arg
can be sent as an “undefined”/“null” segment. Update pathExpr() to keep the
inline template literal behavior while still validating required path params
before building the URL, using the same SdkOperation path-param handling that
encodePath previously enforced.

In `@api/spec/packages/typespec-typescript/src/ZodOperations.tsx`:
- Around line 18-23: Make the query-parameter schema generation in ZodOperations
wire-mode aware so the wire pass does not camelCase keys for query params.
Update the logic around paramObject(queryParams, true) and the related query
schema builders (including *QueryParamsWire and the helpers in ZodOperations) to
preserve wire-format names when generating wire schemas, while still using
toCamelCase only for the non-wire schema path. Ensure the fix covers the
query-params validation flow used after toWire(...).

---

Nitpick comments:
In `@api/spec/packages/aip-client-javascript/.npmignore`:
- Around line 14-19: The npm package ignore list is missing the new tests tree,
so dev-only specs/helpers would still be published. Update the .npmignore entry
set alongside vitest.config.ts and coverage/ to also exclude tests/, keeping the
package publishable only with runtime files.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5b7655d9-228b-4cd6-8ff4-9c0a6feae491

📥 Commits

Reviewing files that changed from the base of the PR and between 7899b41 and 01ede2f.

⛔ Files ignored due to path filters (1)
  • api/spec/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (52)
  • api/spec/AGENTS.md
  • api/spec/package.json
  • api/spec/packages/aip-client-javascript/.gitignore
  • api/spec/packages/aip-client-javascript/.npmignore
  • api/spec/packages/aip-client-javascript/README.md
  • api/spec/packages/aip-client-javascript/src/funcs/addons.ts
  • api/spec/packages/aip-client-javascript/src/funcs/apps.ts
  • api/spec/packages/aip-client-javascript/src/funcs/billing.ts
  • api/spec/packages/aip-client-javascript/src/funcs/currencies.ts
  • api/spec/packages/aip-client-javascript/src/funcs/customers.ts
  • api/spec/packages/aip-client-javascript/src/funcs/defaults.ts
  • api/spec/packages/aip-client-javascript/src/funcs/entitlements.ts
  • api/spec/packages/aip-client-javascript/src/funcs/events.ts
  • api/spec/packages/aip-client-javascript/src/funcs/features.ts
  • api/spec/packages/aip-client-javascript/src/funcs/governance.ts
  • api/spec/packages/aip-client-javascript/src/funcs/invoices.ts
  • api/spec/packages/aip-client-javascript/src/funcs/llmCost.ts
  • api/spec/packages/aip-client-javascript/src/funcs/meters.ts
  • api/spec/packages/aip-client-javascript/src/funcs/planAddons.ts
  • api/spec/packages/aip-client-javascript/src/funcs/plans.ts
  • api/spec/packages/aip-client-javascript/src/funcs/subscriptions.ts
  • api/spec/packages/aip-client-javascript/src/funcs/tax.ts
  • api/spec/packages/aip-client-javascript/src/index.ts
  • api/spec/packages/aip-client-javascript/src/lib/config.ts
  • api/spec/packages/aip-client-javascript/src/lib/encodings.ts
  • api/spec/packages/aip-client-javascript/src/lib/wire.ts
  • api/spec/packages/aip-client-javascript/src/models/operations/tax.ts
  • api/spec/packages/aip-client-javascript/src/models/schemas.ts
  • api/spec/packages/aip-client-javascript/src/models/types.ts
  • api/spec/packages/aip-client-javascript/tests/meters.spec.ts
  • api/spec/packages/aip-client-javascript/tests/wire-helpers.ts
  • api/spec/packages/aip-client-javascript/tests/wire.generated.spec.ts
  • api/spec/packages/aip-client-javascript/tests/wire.spec.ts
  • api/spec/packages/aip-client-javascript/vitest.config.ts
  • api/spec/packages/typespec-typescript/package.json
  • api/spec/packages/typespec-typescript/src/ZodOperations.tsx
  • api/spec/packages/typespec-typescript/src/casing-gate.ts
  • api/spec/packages/typespec-typescript/src/casing.ts
  • api/spec/packages/typespec-typescript/src/components/ZodSchema.tsx
  • api/spec/packages/typespec-typescript/src/components/ZodSchemaDeclaration.tsx
  • api/spec/packages/typespec-typescript/src/emitter.tsx
  • api/spec/packages/typespec-typescript/src/interface-types.ts
  • api/spec/packages/typespec-typescript/src/readme.ts
  • api/spec/packages/typespec-typescript/src/request-types.ts
  • api/spec/packages/typespec-typescript/src/runtime-templates.ts
  • api/spec/packages/typespec-typescript/src/runtime/wire.ts
  • api/spec/packages/typespec-typescript/src/sdk-files.ts
  • api/spec/packages/typespec-typescript/src/ts-types.ts
  • api/spec/packages/typespec-typescript/src/utils.tsx
  • api/spec/packages/typespec-typescript/src/wire-runtime.ts
  • api/spec/packages/typespec-typescript/src/zodBaseSchema.tsx
  • api/spec/packages/typespec-typescript/test/casing.test.ts

Comment thread api/spec/AGENTS.md
Comment on lines +200 to +213
`SDKOptions.validate` (default **off**) turns on schema validation of the actual
snake*case wire payload: the request body after `toWire` (before sending) and the
raw response body before `fromWire`. Validation uses the generated **`…Wire`
schemas** in `models/schemas.ts` — every model and per-op body/response is emitted a
second time in a snake_case "wire" pass (`WireModeContext` in the emitter), keyed by
the raw JSON wire name and made `z.strictObject`, so a wrong-shaped or
leaked-camelCase wire field is **rejected, not silently stripped**. Open models
(record spread, `emitsAsIntersection`, e.g. `baseError`) stay non-strict — strict
would defeat the record arm that exists to accept them. Because the wire pass is the
same emitter walk as the camelCase pass (parameterized by key-casing + strictness +
a separate refkey namespace), the two are structurally identical except for casing,
**by construction** — no runtime schema derivation. A failure throws
`ValidationError`, which `request()` surfaces as `Result.error` (request validation
runs \_inside* the `request()` closure so it does not throw synchronously).

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the broken Markdown formatting here.

snake*case and \_inside* currently render like malformed emphasis, so this part of the contract reads oddly in the docs. I'd switch them to snake_case and _inside_.

As per path instructions, **/*.md: "Assess the documentation for misspellings, grammatical errors, missing documentation and correctness".

Suggested tweak
-`SDKOptions.validate` (default **off**) turns on schema validation of the actual
-snake*case wire payload: the request body after `toWire` (before sending) and the
+`SDKOptions.validate` (default **off**) turns on schema validation of the actual
+`snake_case` wire payload: the request body after `toWire` (before sending) and the
@@
-`ValidationError`, which `request()` surfaces as `Result.error` (request validation
-runs \_inside* the `request()` closure so it does not throw synchronously).
+`ValidationError`, which `request()` surfaces as `Result.error` (request validation
+runs _inside_ the `request()` closure so it does not throw synchronously).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`SDKOptions.validate` (default **off**) turns on schema validation of the actual
snake*case wire payload: the request body after `toWire` (before sending) and the
raw response body before `fromWire`. Validation uses the generated **`…Wire`
schemas** in `models/schemas.ts` — every model and per-op body/response is emitted a
second time in a snake_case "wire" pass (`WireModeContext` in the emitter), keyed by
the raw JSON wire name and made `z.strictObject`, so a wrong-shaped or
leaked-camelCase wire field is **rejected, not silently stripped**. Open models
(record spread, `emitsAsIntersection`, e.g. `baseError`) stay non-strict — strict
would defeat the record arm that exists to accept them. Because the wire pass is the
same emitter walk as the camelCase pass (parameterized by key-casing + strictness +
a separate refkey namespace), the two are structurally identical except for casing,
**by construction** — no runtime schema derivation. A failure throws
`ValidationError`, which `request()` surfaces as `Result.error` (request validation
runs \_inside* the `request()` closure so it does not throw synchronously).
`SDKOptions.validate` (default **off**) turns on schema validation of the actual
`snake_case` wire payload: the request body after `toWire` (before sending) and the
raw response body before `fromWire`. Validation uses the generated **`…Wire`
schemas** in `models/schemas.ts` — every model and per-op body/response is emitted a
second time in a snake_case "wire" pass (`WireModeContext` in the emitter), keyed by
the raw JSON wire name and made `z.strictObject`, so a wrong-shaped or
leaked-camelCase wire field is **rejected, not silently stripped**. Open models
(record spread, `emitsAsIntersection`, e.g. `baseError`) stay non-strict — strict
would defeat the record arm that exists to accept them. Because the wire pass is the
same emitter walk as the camelCase pass (parameterized by key-casing + strictness +
a separate refkey namespace), the two are structurally identical except for casing,
**by construction** — no runtime schema derivation. A failure throws
`ValidationError`, which `request()` surfaces as `Result.error` (request validation
runs _inside_ the `request()` closure so it does not throw synchronously).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/spec/AGENTS.md` around lines 200 - 213, The Markdown in this contract
section is broken because the asterisks in snake*case and \_inside* are being
interpreted as emphasis instead of literal text. Update the AGENTS.md prose near
SDKOptions.validate and request() to use proper inline code or plain Markdown
emphasis for the intended terms, and verify the surrounding wording still reads
correctly after replacing those malformed fragments.

Source: Path instructions

Comment on lines +31 to +35
.then((data) => {
if (client._options.validate) {
assertValid(schemas.listAppsResponseWire, data)
}
return fromWire(data, schemas.listAppsResponse)

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.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

This now strips additive response fields from the runtime payload.

Because fromWire() only materializes schema-declared keys, any server-added fields that used to survive raw .json<T>() are silently lost here even when validation is off. That’s a wire-contract change across the SDK surface for consumers that proxy responses or reach for newly added fields before regenerating. Unless this is intentionally semver-major, the mapper should preserve unknown response keys and only camelize the known ones.

Also applies to: 50-54

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/spec/packages/aip-client-javascript/src/funcs/apps.ts` around lines 31 -
35, The listApps response mapping in apps.ts is dropping server-added fields
because fromWire() only keeps schema-defined keys. Update the listApps
transformation to preserve unknown response properties while still camelizing
and validating the known ones via schemas.listAppsResponseWire and
schemas.listAppsResponse, so additive fields remain available to consumers when
client._options.validate is off.

Comment thread api/spec/packages/aip-client-javascript/src/funcs/plans.ts Outdated
Comment thread api/spec/packages/aip-client-javascript/src/lib/config.ts
Comment on lines +20 to +22
function def(schema: ZodType | undefined): ZodDef | undefined {
return (schema as { def?: ZodDef } | undefined)?.def
}

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.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

set -euo pipefail

# Inspect the target file with line numbers.
sed -n '1,220p' api/spec/packages/aip-client-javascript/src/lib/wire.ts

printf '\n--- search for schema.def and zod internals ---\n'
rg -n "\.def\b|_def\b|schema\.def|schema\._def|ZodType" api/spec/packages/aip-client-javascript -S

printf '\n--- locate package metadata for zod version ---\n'
rg -n '"zod"|zod' api/spec/packages/aip-client-javascript/package.json api/spec/packages/aip-client-javascript/**/package.json -S || true

Repository: openmeterio/openmeter

Length of output: 11966


🏁 Script executed:

set -euo pipefail

# Find the Zod package version and any local wrappers around it.
git ls-files | rg '(^|/)package\.json$|(^|/)wire\.ts$|(^|/)zod' -n

printf '\n--- package.json files mentioning zod ---\n'
for f in $(git ls-files '**/package.json'); do
  if rg -q '"zod"|zod' "$f"; then
    echo "### $f"
    sed -n '1,220p' "$f"
    echo
  fi
done

printf '\n--- broader search for definition access patterns ---\n'
rg -n "\.def\b|_def\b|definition\b" api/spec/packages/aip-client-javascript -S

Repository: openmeterio/openmeter

Length of output: 7878


🏁 Script executed:

python3 - <<'PY'
from pathlib import Path
p = Path('api/spec/packages/aip-client-javascript/src/lib/wire.ts')
print(p.read_text())
PY

Repository: openmeterio/openmeter

Length of output: 9608


🌐 Web query:

Zod 4 schema internals def _def public property docs

💡 Result:

In Zod 4, the internal structure of schemas has been significantly refactored compared to Zod 3. All Zod 4 schema instances contain a private _zod property [1][2], which acts as the container for the schema's internals [1]. The _def property, which was the primary way to access schema internals in Zod 3, has been deprecated in Zod 4 in favor of a cleaner internal structure [3]. While _def may still be present in some instances for backward compatibility, it is officially deprecated and not guaranteed to be consistent across all schema types [3]. Key points regarding schema internals in Zod 4: 1. The _zod Property: All zod/v4/core classes use a single, private _zod property [1]. This object encapsulates the schema's definition, traits, and other metadata [2]. 2. The def Property: The canonical way to access a schema's definition within the _zod object is through ._zod.def [1]. This property contains the configuration object passed to the schema constructor and is designed to be JSON-serializable [1]. It allows for programmatic inspection of schema types (e.g., def.type) and configuration [1]. 3. Deprecation of _def: Because of the move to the _zod internal object, accessing schema._def is officially discouraged [3]. In some complex cases or specific schema types (like ZodDefault), _def may still be the only way to retrieve certain properties, but reliance on it is considered brittle and not a stable public API [3]. 4. Schema Identification: Developers are advised against using _def identity checks (e.g., schemaA._def === schemaB._def) to identify schemas, especially since methods like .describe() or .meta() often clone the schema and create new _def objects [4]. Instead, metadata should be handled using the dedicated .meta() and .register() methods [5]. For developers building tools or libraries that need to traverse schemas, the recommended pattern is to cast the schema to $ZodTypes and inspect the ._zod.def property to discriminate between types [1].

Citations:


Read Zod internals from schema._zod.def here. Zod 4 stores schema definitions under _zod.def, so schema.def will stay undefined and walk() will skip the object/union/record paths that rename payload keys.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/spec/packages/aip-client-javascript/src/lib/wire.ts` around lines 20 -
22, The helper def currently reads the wrong Zod internals, so it never returns
the schema definition for Zod 4 types. Update def in wire.ts to read from
schema._zod.def instead of schema.def, and keep the rest of the walk() logic
unchanged so object, union, and record payload key renaming paths are reached.

Comment thread api/spec/packages/typespec-typescript/src/casing-gate.ts Outdated
Comment on lines +102 to +105
// Fail the build if any wire key is not recoverable from its camelCase public
// form by the deterministic casing rule, before emitting anything that relies
// on it.
assertCasingDerivable(context.program, models, operations)

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The casing gate still misses camelCase collisions.

Right now this only proves each wire key round-trips on its own. It still allows two distinct wire names to collapse onto the same public key — e.g. foo_bar and fooBar both become fooBar — and the new public emitters now use that camelized name for model fields and query params. In that case the generated types/schemas will silently duplicate or shadow one side instead of failing the build. Please extend the gate to reject duplicate camelized names per model and per query object before emission.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/spec/packages/typespec-typescript/src/emitter.tsx` around lines 102 -
105, The current casing check in assertCasingDerivable only verifies that each
wire key can be deterministically recovered, but it does not catch collisions
where two different wire names map to the same camelCase public name. Extend the
gate around assertCasingDerivable in emitter.tsx to also detect duplicate
camelized names before emission, and fail the build when a model’s fields or a
query object’s parameters would collapse to the same public key. Use the
existing model and operation traversal to compare the derived public names for
each group and reject any duplicates before the public emitters run.

Comment on lines +20 to +22
function def(schema: ZodType | undefined): ZodDef | undefined {
return (schema as { def?: ZodDef } | undefined)?.def
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
node --input-type=module <<'NODE'
import { z } from 'zod'

const objectSchema = z.object({ fooBar: z.string() })
const literalSchema = z.literal('customer')

console.log({
  objectDef: objectSchema.def?.type,
  objectZodDef: objectSchema._zod?.def?.type,
  literalValue: literalSchema.value,
  literalDefValue: literalSchema.def?.value,
  literalDefValues: literalSchema.def?.values,
  literalZodDefValue: literalSchema._zod?.def?.value,
  literalZodDefValues: literalSchema._zod?.def?.values,
})
NODE

Repository: openmeterio/openmeter

Length of output: 1159


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== package references ==\n'
rg -n '"zod"|zod' api/spec/packages/typespec-typescript/package.json api/spec/packages/typespec-typescript -g '!**/dist/**' -g '!**/build/**' || true

printf '\n== relevant source ==\n'
sed -n '1,260p' api/spec/packages/typespec-typescript/src/runtime/wire.ts

printf '\n== nearby callers ==\n'
rg -n "literalValue\\(|def\\(" api/spec/packages/typespec-typescript/src/runtime -g '!**/dist/**' || true

Repository: openmeterio/openmeter

Length of output: 22040


🌐 Web query:

Zod 4.4.3 schema def _zod.def literal value internal property classic runtime

💡 Result:

In Zod 4.4.3, accessing internal properties of a schema, such as the literal values of a z.literal() schema, is considered an advanced or internal use case rather than a supported public API [1][2]. Key points regarding schema introspection and internal properties in Zod 4: 1. Introspection APIs: Zod does not provide a single, unified public API for schema introspection. Instead, it relies on specific instance properties (e.g., .shape for objects, .element for arrays, .unwrap() for optional/nullable wrappers) [3][2]. 2. Internal Definitions (.def vs ._def): Zod schemas maintain their configuration within a def property [4][5]. In Zod 4, the primary property is .def, and ._def is deprecated (often retained for internal compatibility) [5][6]. These properties contain the schema's type definition and metadata, such as the values set for literals [4][7]. 3. Breaking Changes for Literals: In Zod 4, z.literal() schemas no longer expose a single .value property. This was replaced by a .values property, which is a Set to support cases where a literal schema might represent multiple values [8]. 4. Accessing Literal Values: Because these internal structures are subject to change and not part of the stable public API, code that relies on accessing schema.def.values is fragile and may break across minor or patch versions [9][2]. When introspection is required, the recommended approach is often to perform custom logic (such as checking instanceof on specific Zod classes) or to write a utility function that traverses the schema structure using these internal properties with appropriate checks [1][9][2]. For Zod 4.4.3 specifically, you can inspect the schema's internal definition structure by exploring the def object [4][7], but you should be aware that these properties are explicitly marked as @internal or are considered implementation details not guaranteed to remain stable [4][7].

Citations:


Use def.values for literal discriminators
def() matches Zod 4.4.3, but literalValue() is still reading schema.value. Zod 4 literals keep their allowed values on def.values, so the discriminator map stays empty and discriminated unions won’t be renamed correctly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/spec/packages/typespec-typescript/src/runtime/wire.ts` around lines 20 -
22, The literal discriminator lookup is still reading the old Zod shape, so
discriminated union renaming won’t work. Update the discriminator extraction in
the runtime wire helpers by adjusting the `literalValue()` logic to read from
`def.values` via the existing `def()` helper instead of `schema.value`, keeping
the change aligned with `ZodDef` and the `def()` function.

Comment thread api/spec/packages/typespec-typescript/src/sdk-files.ts
Comment thread api/spec/packages/typespec-typescript/src/ZodOperations.tsx
function literalValue(schema: ZodType | undefined): unknown {
const s = unwrap(schema)
if (def(s)?.type === 'literal') {
return (s as { value?: unknown }).value

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.

P1 Literal Lookup Still Fails

literalValue still reads the discriminator literal from the old top-level schema.value slot. The generated Zod v4 discriminated schemas use z.literal(...) inside the variant shape, so this lookup can still return undefined and leave variantsByDiscriminator empty. When fromWire maps a valid discriminated response such as an app response, selectVariant cannot choose the branch and returns the raw object, leaving wire keys like account_id on the public SDK result instead of accountId.

Context Used: api/spec/AGENTS.md (source)

Prompt To Fix With AI
This is a comment left during a code review.
Path: api/spec/packages/typespec-typescript/src/runtime/wire.ts
Line: 255

Comment:
**Literal Lookup Still Fails**

`literalValue` still reads the discriminator literal from the old top-level `schema.value` slot. The generated Zod v4 discriminated schemas use `z.literal(...)` inside the variant shape, so this lookup can still return `undefined` and leave `variantsByDiscriminator` empty. When `fromWire` maps a valid discriminated response such as an app response, `selectVariant` cannot choose the branch and returns the raw object, leaving wire keys like `account_id` on the public SDK result instead of `accountId`.

**Context Used:** api/spec/AGENTS.md ([source](https://app.greptile.com/openmeter/github/openmeterio/openmeter/-/custom-context?memory=28ba6068-00f9-4629-9b78-8e49cc802858))

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/spec/packages/aip-client-javascript/src/funcs/customers.ts`:
- Around line 75-80: The path-building IIFE in the customer SDK methods throws
before request(...) is invoked, so missing customerId/creditGrantId escapes the
SDK error wrapper. Move the missing-path validation and path construction inside
the request callback for the affected customer-related operations in
customers.ts so request(...) can return a Promise<Result<...>> consistently. Use
the existing request helper and the generated method bodies as the lookup
points, and apply the same pattern to all other listed endpoint builders in this
file.

In `@api/spec/packages/typespec-typescript/src/runtime/wire.ts`:
- Around line 151-155: The wire mapper is returning null-prototype records
instead of plain objects. Update the record-building logic in the wire
conversion path (the `wire.ts` code that iterates `Object.entries(record)` and
any similar helper around the other flagged location) to use normal plain
objects, ideally by building from entries rather than `Object.create(null)`, so
callers keep standard `Object.prototype` behavior while still preserving
`__proto__` as data.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b0d601d8-ef95-439a-9285-2514d445fb3c

📥 Commits

Reviewing files that changed from the base of the PR and between 01ede2f and 3dc5afd.

📒 Files selected for processing (22)
  • api/spec/packages/aip-client-javascript/src/funcs/addons.ts
  • api/spec/packages/aip-client-javascript/src/funcs/apps.ts
  • api/spec/packages/aip-client-javascript/src/funcs/billing.ts
  • api/spec/packages/aip-client-javascript/src/funcs/currencies.ts
  • api/spec/packages/aip-client-javascript/src/funcs/customers.ts
  • api/spec/packages/aip-client-javascript/src/funcs/entitlements.ts
  • api/spec/packages/aip-client-javascript/src/funcs/features.ts
  • api/spec/packages/aip-client-javascript/src/funcs/invoices.ts
  • api/spec/packages/aip-client-javascript/src/funcs/llmCost.ts
  • api/spec/packages/aip-client-javascript/src/funcs/meters.ts
  • api/spec/packages/aip-client-javascript/src/funcs/planAddons.ts
  • api/spec/packages/aip-client-javascript/src/funcs/plans.ts
  • api/spec/packages/aip-client-javascript/src/funcs/subscriptions.ts
  • api/spec/packages/aip-client-javascript/src/funcs/tax.ts
  • api/spec/packages/aip-client-javascript/src/index.ts
  • api/spec/packages/aip-client-javascript/src/lib/wire.ts
  • api/spec/packages/aip-client-javascript/src/models/schemas.ts
  • api/spec/packages/aip-client-javascript/tests/wire.spec.ts
  • api/spec/packages/typespec-typescript/src/ZodOperations.tsx
  • api/spec/packages/typespec-typescript/src/casing-gate.ts
  • api/spec/packages/typespec-typescript/src/runtime/wire.ts
  • api/spec/packages/typespec-typescript/src/sdk-files.ts
✅ Files skipped from review due to trivial changes (1)
  • api/spec/packages/aip-client-javascript/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (17)
  • api/spec/packages/aip-client-javascript/src/funcs/entitlements.ts
  • api/spec/packages/aip-client-javascript/src/funcs/invoices.ts
  • api/spec/packages/aip-client-javascript/src/funcs/apps.ts
  • api/spec/packages/typespec-typescript/src/ZodOperations.tsx
  • api/spec/packages/aip-client-javascript/src/funcs/addons.ts
  • api/spec/packages/typespec-typescript/src/casing-gate.ts
  • api/spec/packages/aip-client-javascript/src/funcs/billing.ts
  • api/spec/packages/aip-client-javascript/tests/wire.spec.ts
  • api/spec/packages/aip-client-javascript/src/funcs/currencies.ts
  • api/spec/packages/aip-client-javascript/src/funcs/tax.ts
  • api/spec/packages/aip-client-javascript/src/funcs/features.ts
  • api/spec/packages/aip-client-javascript/src/funcs/planAddons.ts
  • api/spec/packages/aip-client-javascript/src/funcs/subscriptions.ts
  • api/spec/packages/aip-client-javascript/src/funcs/llmCost.ts
  • api/spec/packages/aip-client-javascript/src/funcs/plans.ts
  • api/spec/packages/typespec-typescript/src/sdk-files.ts
  • api/spec/packages/aip-client-javascript/src/funcs/meters.ts

Comment thread api/spec/packages/aip-client-javascript/src/funcs/customers.ts
Comment on lines +151 to +155
const valueSchema = hasRenamableShape(d.valueType) ? d.valueType : undefined
const out: Record<string, unknown> = Object.create(null)
for (const [key, value] of Object.entries(record)) {
out[key] = valueSchema ? walk(value, valueSchema, dir, depth + 1) : value
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
node --input-type=module <<'NODE'
const out = Object.fromEntries([['__proto__', 'safe'], ['constructor', 1]])
console.log(Object.getPrototypeOf(out) === Object.prototype)
console.log(Object.prototype.hasOwnProperty.call(out, '__proto__'))
console.log(typeof out.hasOwnProperty)

const nullProto = Object.create(null)
nullProto.foo = 1
console.log(typeof nullProto.hasOwnProperty)
NODE

Repository: openmeterio/openmeter

Length of output: 188


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file structure first
ast-grep outline api/spec/packages/typespec-typescript/src/runtime/wire.ts --view expanded

# Read the relevant region around the reported lines
sed -n '120,210p' api/spec/packages/typespec-typescript/src/runtime/wire.ts

# Find other uses of Object.create(null) in this runtime file
rg -n "Object\.create\(null\)|Object\.fromEntries|hasOwnProperty|instanceof Object" api/spec/packages/typespec-typescript/src/runtime/wire.ts

Repository: openmeterio/openmeter

Length of output: 4934


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,360p' api/spec/packages/typespec-typescript/src/runtime/wire.ts

printf '\n--- usages ---\n'
rg -n "\btoWire\(|\bfromWire\(|assertValid\(|wire\.ts" api/spec/packages/typespec-typescript/src -g '!**/dist/**' -g '!**/build/**'

Repository: openmeterio/openmeter

Length of output: 12679


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for generated-client guidance and for places where these helpers are used in serialization paths.
rg -n "generated SDK|regenerate it from TypeSpec|toWire\(|fromWire\(" api/spec/packages -g '!**/dist/**' -g '!**/build/**' -g '!**/node_modules/**'

Repository: openmeterio/openmeter

Length of output: 21601


Return plain objects from the wire mapper api/spec/packages/typespec-typescript/src/runtime/wire.ts:152,175

Object.create(null) turns these SDK results into null-prototype values, which drops normal Object.prototype behavior for callers. Object.fromEntries(...) keeps __proto__ as data while preserving a standard object prototype.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/spec/packages/typespec-typescript/src/runtime/wire.ts` around lines 151 -
155, The wire mapper is returning null-prototype records instead of plain
objects. Update the record-building logic in the wire conversion path (the
`wire.ts` code that iterates `Object.entries(record)` and any similar helper
around the other flagged location) to use normal plain objects, ideally by
building from entries rather than `Object.create(null)`, so callers keep
standard `Object.prototype` behavior while still preserving `__proto__` as data.

Source: Coding guidelines

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

Labels

release-note/feature Release note: Exciting New Features

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant