fix(cli): emit explicit null for required nullable properties in generated examples - #17565
fix(cli): emit explicit null for required nullable properties in generated examples#17565devin-ai-integration[bot] wants to merge 4 commits into
Conversation
…rated examples Co-Authored-By: bot_apk <apk@cognition.ai>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
There was a problem hiding this comment.
AI Review Summary
The fix is small and correct in spirit: nullable is no longer conflated with optional in the OpenAPI example factory, and empty nullable containers now serialize as explicit null. Snapshots line up with the described behavior. One behavioral concern worth calling out: jsonExample: null for nullable containers now applies to all empty nullable containers, including nullable request bodies (see nullable-request-body.json / java-nullable-named-request-types.json), which changes generated wire tests from "no body" to "body is literal null" — confirm that's intended for all generators, not just TS.
- 🟡 1 warning(s)
To request another review, comment /ai-review on this pull request.
| }, | ||
| "jsonExample": null |
There was a problem hiding this comment.
🟡 warning
This is the case I'd double-check: the change to generateEmptyContainerExample also affects whole nullable request bodies, not just properties. Previously the example body had no jsonExample, now it's literal null, which for some generators means sending null as the HTTP body rather than omitting it. Worth verifying seed output for a non-TS generator (java/python) before merging, since only ts-sdk/required-nullable was regenerated per the description.
There was a problem hiding this comment.
Investigated — you were right that whole nullable request bodies were affected, though the null came from a different caller than the review summary assumed. Fixed in 9f6c340 by scoping the change; the request-body snapshots are now identical to main.
Where it actually came from. generateEmptyContainerExample has two callers: the nullable case of generateContainerExample under skipOptionalProperties, and the failure fallback in generateTypeReferenceExample. The jsonExample: null on /optional-request-body/path_param came from the first one: generateEndpointExample's case "reference" calls generateTypeReferenceExample with the body type itself (nullable<PlainObject>, currentDepth: 0, skipOptionalProperties: skipOptionalRequestProperties), so a top-level nullable body hit exactly the branch I had changed. Reverting only the fallback left the snapshot at jsonExample: null — that's how I confirmed the attribution.
What changed:
generateEmptyContainerExample'snullablecase is back tojsonExample: undefined(no change vs.mainfor the failure fallback or the depth-limit path).- The explicit
nullnow lives inline in thenullablebranch ofgenerateContainerExample, i.e. only on the property path. generateEndpointExample'scase "reference"short-circuits a whole nullable request body underskipOptionalRequestPropertiesback to the previous "nojsonExample" example, so anullable<T>body stays omitted rather than becoming a literalnullbody.
Evidence: every request-body example reverted. After regenerating with vitest -u, these files are byte-identical to main again:
--- a/.../ir-generator-tests/.../test-definitions/nullable-request-body.json
+++ b/.../ir-generator-tests/.../test-definitions/nullable-request-body.json
@@ -561,8 +561,7 @@
"inline": null
}
}
- },
- "jsonExample": null
+ }
},…and the same removal in java-nullable-named-request-types.json, v3-sdks/primitive-array.json and v3-sdks/primitive-array-complex.json (all three of those were "request": { "jsonExample": null, "shape": { "container": ... } }, i.e. reference bodies). Full check:
$ git diff --merge-base origin/main -- .../ir-generator-tests .../v3-importer-tests | grep -B30 '^+.*"jsonExample": null' | grep -c '"type": "reference"'
0
Every remaining added jsonExample: null in this PR is an inlined-request-body property or an object property (content-type.json: "jsonExample": { "requiredField": "requiredField", "nullableString": null }, nullable-optional.json, required-nullable.json), which is the intended fix.
Cross-language seed evidence. Regenerated python-sdk and go-sdk for nullable-request-body, nullable-optional and nullable, plus ts-sdk/nullable-request-body: the generated-source diff is empty for all of them (the only churn was .fern/metadata.json invokedBy: ci → manual and poetry.lock ordering from running seed locally, which is not committed). So no generator's body handling changes — including the Java respect-optional-request-body path you cited, which keys off exactly this "no body" example.
seed/python-sdk/required-nullable and seed/go-sdk/required-nullable were stale (that fixture gained a required-nullable property in this PR), so they are regenerated and committed here; their only diffs are the intended new required_nullable_text property in snippets/reference/types.
Java seed could not be regenerated on this machine: ./gradlew can't resolve its build plugins because Maven Central answers 429 Too Many Requests (Could not GET '.../guava-32.0.1-jre.pom'. Received status code 429), so fernapi/fern-java-sdk:local never builds. CI's java seed job covers it, and since the java nullable IR snapshots are now identical to main, the Java generator sees no input change.
ts-sdk/required-nullable's wire test still asserts the original fix (required_nullable_text present in the request body), so the property-level behavior this PR is about is unchanged.
There was a problem hiding this comment.
🔍 Devin Review: 1 flag
Not posted on this PR by your GitHub settings — view it in Devin Review. (Configure)
Co-Authored-By: bot_apk <apk@cognition.ai>
… examples Co-Authored-By: bot_apk <apk@cognition.ai>
Docs Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on
Docs generation runs |
SDK Generation Benchmark ResultsComparing PR branch against median of 5 nightly run(s) on Full benchmark table (click to expand)
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 |
…xamples Scopes the explicit null to nullable properties; a reference request body typed nullable<T> keeps its previous behavior of having no jsonExample. Co-Authored-By: bot_apk <apk@cognition.ai>
|
Review — no breaking changes found, with one thing worth confirming. The semantic change is real but corrective: required
Blast radius note (not a blocker): Generated Go/Python/TS changes are additive. Approving. |
Description
Linear ticket: Refs
Auto-generated (Fern-synthesized) examples omitted properties that are required and nullable, so generated wire tests could send a body missing a key the serializer requires (
JsonError: Missing required key "webhook"oncenoSerdeLayer: false). Root cause: both example generators conflated "nullable" with "optional".ExampleTypeFactory.getAllRequiredPropertiesskipped any property whose resolved schema wasnullable, so it was never treated as required:Optional properties are always wrapped in
optional(an optional nullable property isoptional<nullable<T>>), so excludingnullablewas only ever wrong. Now onlyoptionalis excluded.jsonExample: undefined, which disappears from the serialized JSON example. Thenullablebranch ofgenerateContainerExamplenow emitsjsonExample: nullunderskipOptionalProperties, so the key is present with an explicitnull.Scoped to properties:
generateEmptyContainerExample(the failure fallback ingenerateTypeReferenceExampleand the depth-limit path) still returnsjsonExample: undefined, andgenerateEndpointExample's reference-request-body case keeps a wholenullable<T>request body without ajsonExample, so generators that key off "no body" (e.g. Java'srespect-optional-request-body) keep omitting the body instead of sending a literalnull. The IR snapshots for reference request bodies are byte-identical tomain.Optional-property omission is unchanged:
optional<nullable<T>>is still omitted when optionals are skipped.Response examples used the same code paths and are covered by the new tests.
Secondary note (not addressed here): the failing generated wire test contradicts the generated types but still ran, because vitest transpiles wire tests with esbuild and does not type-check them. Worth considering whether generated tests should be type-checked in CI.
Changes Made
ExampleTypeFactory: nullable no longer implies optional when computing required properties.generateContainerExample: a nullable property serializes to an explicitnullinstead of being dropped; the empty-container fallback is unchanged.generateEndpointExample: a whole nullable reference request body stays omitted.required-nullabletest definition (Fern + OpenAPI) with a required nullable request property; regenerated IR snapshots and thets-sdk,python-sdkandgo-sdkrequired-nullableseed output.Before (IR for a required nullable
webhook):{ "processor_token": "processor_token" }After:
{ "processor_token": "processor_token", "webhook": null }Testing
ExampleTypeFactory.test.ts(request emitsnull, optional nullable still omitted, response includes the property) andgenerateTypeReferenceExample.test.ts.fern ir) before/after; seed tests pass forts-sdk,python-sdkandgo-sdkonrequired-nullable,nullable-request-body,nullable-optionalandnullable, with an empty generated-source diff for every fixture exceptrequired-nullable;pnpm compile, biome lint/format, and the ir-generator-tests / ir-migrations / v3-importer / openapi-ir-to-fern suites pass. Java seed could not be run locally (Maven Central returns HTTP 429 for the generator's gradle build).Link to Devin session: https://app.devin.ai/sessions/ec7249febead4032bda20ab29448797a
Open in Devin Desktop: https://app.devin.ai/desktop/session/ec7249febead4032bda20ab29448797a?variant=devin