Skip to content

fix(typescript): match SDK serde default, quote non-identifier keys and add file placeholder in dynamic snippets - #18323

Open
willkendall01 wants to merge 2 commits into
mainfrom
devin/1792082258-ts-dynamic-snippets-auth0-fixes
Open

willkendall01 wants to merge 2 commits into
mainfrom
devin/1792082258-ts-dynamic-snippets-auth0-fixes

Conversation

@willkendall01

@willkendall01 willkendall01 commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Description

Linear ticket: N/A

TypeScript dynamic snippets didn't compile against SDKs generated from the same config:

  • resolveNoSerdeLayer treated an omitted noSerdeLayer as false. The SDK generator treats it as true, so snippets used camelCase request properties (perPage) while the SDK types use wire names (per_page).
  • formatOriginalPropertyName only quoted names containing -, so keys like org.iso.18013.5.1.mDL or x5t#S256 came out unquoted, which is a syntax error.
  • A required file field with no example value was dropped (TypeLiteral.nop()), so the call was missing a required property.

Changes Made

  • resolveNoSerdeLayer now defaults to true, the same as the SDK generator (serdeLayer still takes precedence). This changes dynamic snippet output only, not generated SDK code.
  • Any property name that doesn't match /^[A-Za-z_$][A-Za-z0-9_$]*$/ is now quoted with JSON.stringify.
  • A file field with no example value now gets fs.createReadStream("/path/to/your/file").
  • Blob literals are always emitted as new Blob([...]). The noSerdeLayer branch emitted a bare string, which core.file.Uploadable doesn't accept. With the new default, that branch would have run for most configs.
  • Changelog: generators/typescript/sdk/changes/unreleased/dynamic-snippets-sdk-defaults-and-files.yml
  • Updated README.md generator (if applicable)

Testing

  • Unit tests added/updated (@fern-api/typescript-dynamic-snippets snapshots: wire property names, file placeholder)
  • Manual testing completed: rebuilt the package locally and ran a dynamic-IR repro of a real API. The noSerdeLayer-omitted, dotted-key and required-file checks fail on the published package and pass on this build.

Link to Devin session: https://app.devin.ai/sessions/e71a3ab9f283405cb547310ce6dac721
Open in Devin Desktop: https://app.devin.ai/desktop/session/e71a3ab9f283405cb547310ce6dac721?variant=devin
Requested by: @willkendall01


Devin Review

… non-identifier keys, add file placeholder

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".

  • Disable automatic comment, CI, and merge conflict monitoring

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

AI Review Summary

The change flips the dynamic-snippet noSerdeLayer default to true, quotes non-identifier property keys, and emits an fs.createReadStream placeholder for missing file values. The default flip is behaviorally correct per the SDK generator, but note the snapshot now shows maybe_file (an optional file property) receiving a placeholder, which looks like an unintended widening beyond required files; worth confirming. Otherwise the regex/JSON.stringify quoting and the Blob unification look sound.

  • 🟡 1 warning(s)
  • 🔵 1 suggestion(s)

Review Scope

  • Mode: Full pull request
  • Range: Full pull request through fbb0144d13a039ada15dbd175cdd7ac77b8a8542
  • Current range coverage: Complete
  • Cumulative pull request coverage: Complete

This review is complete for the current scope. Addressing all critical and warning findings above satisfies the review; suggestions are optional. Request another review only if subsequent changes introduce new behavior or materially expand the pull request's scope.

const fileValue = this.context.getSingleFileValue({ property, record });
if (fileValue == null) {
return ts.TypeLiteral.nop();
return this.context.getFilePlaceholder();

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.

🟡 warning

This branch fires for any file property with no example value, including optional ones. The snapshot confirms it: maybe_file: fs.createReadStream("/path/to/your/file") now shows up in the file-upload snippet even though the example omitted it. The PR description says the goal was to stop dropping required file fields; for optional ones the previous nop() (omit the key) was the right output. Consider gating on property.isOptional/required-ness and keeping the placeholder only for required properties.

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.

This is deliberate. The dynamic IR's FileUploadRequestBodyProperty.file is only a NameAndWireValue, with no optional/required flag, so the snippet generator can't tell maybe_file from file. A placeholder on an optional field still compiles and is a valid call. Leaving out a required one doesn't compile. The Python and Go dynamic snippets do the same thing (they emit a placeholder for any file field without an example value). If optionality is added to the IR later, this can be limited to required fields.

import { DynamicTypeLiteralMapper } from "./DynamicTypeLiteralMapper.js";
import { FilePropertyMapper } from "./FilePropertyMapper.js";

const VALID_IDENTIFIER_REGEX = /^[A-Za-z_$][A-Za-z0-9_$]*$/;

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.

🔵 suggestion

VALID_IDENTIFIER_REGEX accepts JS reserved words (class, default, function), which are legal as unquoted object-literal keys, so output stays valid — just flagging that the name is slightly aspirational. Renaming to OBJECT_KEY_IDENTIFIER_REGEX would make the intent (valid property key, not valid binding identifier) clearer.

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.

Agreed, reserved words are valid unquoted keys, so the output is correct. I'm keeping the name to avoid churn. The comment at the call site already says it's about property keys.

@devin-ai-integration devin-ai-integration 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.

👀 1 finding needs your review

Devin fixed 1 of 2 findings on fbb0144. Click a finding below to jump to its comment.

For your review (1)

Fixed by Devin (1)

  • Wire-name overrides break default snippets

View all findings in Devin Review

Devin Review

…serde layer in dynamic snippets

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

@devin-ai-integration devin-ai-integration 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.

Devin Review found 1 potential issue.

Devin Review

const fileValue = this.context.getSingleFileValue({ property, record });
if (fileValue == null) {
return ts.TypeLiteral.nop();
return this.context.getFilePlaceholder();

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.

🔴 Absent optional files become failed uploads

When an optional file is absent, getSingleFileProperty inserts a stream for /path/to/your/file. The SDK uploads that nonexistent path instead of omitting the file, so the snippet fails when run.

Learn more

File-upload properties can be optional in the generated SDK; its request interface marks maybe_file optional and its upload method skips absent values. getSingleFileValue returns undefined for both omitted properties and invalid values. The unconditional fallback therefore inserts a stream even when the example intentionally omits an optional file. A missing placeholder path later fails during upload.

Example: An example supplies file: "Hello" but omits optional maybe_file. The snippet emits maybe_file: fs.createReadStream("/path/to/your/file") instead of omitting maybe_file.

Recommended fix: Preserve omission for optional files while inserting a placeholder only for required files with no example. Carry file optionality into the dynamic IR if it is not currently available, and avoid replacing invalid supplied values with a placeholder.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

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.

Same limitation as the earlier thread. The dynamic IR's file properties don't say whether they're optional, so this can't be limited to required files without an IR change, and that would be a separate CLI/IR PR. A placeholder on an optional file still compiles and is meant to be replaced, while leaving out a required one doesn't compile. Python and Go do the same. Leaving the decision on scope to a maintainer.

The other finding from this review (name overrides without a serde layer) is fixed in 95c153c: query params, headers, file fields and object properties now use the wire value, as the SDK does.

@github-actions

Copy link
Copy Markdown
Contributor

SDK Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-10-15T04:05:56Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
ts-sdk square 138s (n=5) 139s (n=5) 124s -14s (-10.1%)

main (generator): generator-only time via --skip-scripts (includes Docker image build, container startup, IR parsing, and code generation — this is the same Docker-based flow customers use via fern generate). main (E2E): full customer-observable time including build/test scripts (nightly baseline, informational). Delta is computed against generator-only baseline.
⚠️ = generation exited with a non-zero exit code (timing may not reflect a successful run).
Baseline from nightly runs on main (latest: 2026-10-15T04:05:56Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-10-15 17:22 UTC

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