Skip to content

fix(cli): preserve explicit null examples so Go wire tests match generated models - #17427

Open
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1786738798-go-nullable-wire-tests
Open

fix(cli): preserve explicit null examples so Go wire tests match generated models#17427
devin-ai-integration[bot] wants to merge 2 commits into
mainfrom
devin/1786738798-go-nullable-wire-tests

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Description

Two independent fixes, one per commit.

1. fix(cli): explicit null examples were replaced with a placeholder object.

The reporter's framing (nullable scalars break Go wire tests) was close but not the mechanism — nullability alone is not the trigger, and WireMock is not inventing anything. WireMock.convertToWireMock copies the IR example's jsonExample verbatim, so the bad body already exists in the IR. It comes from ExampleTypeFactory: for an object with additionalProperties: true, properties not produced by the declared-property pass are re-built through SchemaWithExample.unknown, and the unknown branch ignored an explicit null and fell through to the placeholder:

 case "unknown":
+    if (example === null) {
+        return FullExample.null({});
+    }
     if (example != null) { ... }
     ...
     return FullExample.map([{ key: "key", value: "value" }]);   // <-- {"key": "value"}

So logo: null in the spec example became "logo": {"key": "value"} in wiremock-mappings.json, while the generated model stayed correctly *string — hence json: cannot unmarshal object into Go struct field ... of type string. The model side was right; the example side was wrong, so only the example side changed.

This is not a Go-specific defect: go-v2, python-v2, ruby-v2, php, and rust all generate wire mocks from the same @fern-api/mock-utils + IR examples, so all of them would mock the same object. Go is just the first generator with wire tests on by default over a spec that hits the additionalProperties path. The fix is in the shared OpenAPI importer, so all of them get it.

2. fix(go): doubled /vN module suffix. Confirmed genuine. Both the native generator (maybeAppendVersionSuffix) and go-v2 (maybeAppendMajorVersionSuffix) only skipped appending when the existing suffix equalled the release major, so importPath: github.com/plaid/plaid-go/v46 released as v34.0.0 produced module github.com/plaid/plaid-go/v46/v34. Now any existing /vN suffix wins.

Changes Made

  • ExampleTypeFactory: preserve an explicit null for unknown schemas instead of emitting {"key": "value"}.
  • New seed fixture go-nullable-wire-tests (nullable string / nullable int / nullable nested object, explicit null response example, additionalProperties: true) — the only go-sdk fixture with enableWireTests: true besides the existing ones; the global default is untouched.
  • Go v1 + v2: don't append a major version suffix when the configured import/module path already ends in /vN.
  • Changelogs under packages/cli/cli/changes/unreleased/ and generators/go/sdk/changes/unreleased/.
  • Updated README.md generator (if applicable)

Testing

  • Unit tests added/updated — ExampleTypeFactory.test.ts (explicit-null cases), resolveRootImportPath.test.ts (go-v2 suffix), cmd_test.go (go v1 suffix, incl. /v46 + release v34/v46).
  • Manual testing completed — minimal standalone spec generated locally, WireMock started from the generated wiremock/docker-compose.test.yml:
    • before: json: cannot unmarshal object into Go struct field ... of type string
    • after: mappings contain "logo": null, "rank": null and go test ./... passes.
  • pnpm seed test --generator go-sdk --fixture go-nullable-wire-tests --skip-scripts --local → 1/1 passed.
  • Full pnpm test green, and no committed IR/JSON-schema snapshot changed across the 58 OpenAPI-based test definitions — i.e. the example fix is inert for every existing fixture.
  • pnpm check (biome), gofmt, go build ./..., go vet.

Link to Devin session: https://app.devin.ai/sessions/ee05b7f0ee07445c97a64a00db1d4d5a


Open in Devin Review

devin-ai-integration Bot and others added 2 commits August 14, 2026 20:20
A null example routed through an unknown schema (e.g. a declared property on an object with additionalProperties: true) was replaced with the {"key": "value"} placeholder, so generated Go wire tests mocked an object where the model expects a nullable scalar. Adds a go-sdk seed fixture covering nullable scalars with wire tests enabled.

Co-Authored-By: bot_apk <apk@cognition.ai>
A configured importPath or module.path that already ends in /vN is left alone, so releasing v34.0.0 with importPath github.com/acme/acme-go/v46 no longer produces github.com/acme/acme-go/v46/v34.

Co-Authored-By: bot_apk <apk@cognition.ai>
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@nitpickybot nitpickybot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

AI Review Summary

Two targeted fixes: preserve explicit null for unknown schemas in the OpenAPI example factory, and stop doubling /vN module suffixes in both Go generators. The logic changes look correct; my concerns are the newly-permissive suffix regex (matches v0/v01, and silently discards a genuine major-version bump) and a weak test assertion that would pass even if the property were dropped entirely.

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

Comment on lines +591 to +598
// majorVersionSuffixPattern matches a Go major version suffix, e.g. "v2".
var majorVersionSuffixPattern = regexp.MustCompile(`^v[0-9]+$`)

// maybeAppendVersionSuffix appends the given version suffix to the importPath,
// unless the importPath already ends in a major version suffix. The configured
// suffix wins, even if it doesn't match the version being released.
func maybeAppendVersionSuffix(importPath string, version string) string {
if path.Base(importPath) == version {
if majorVersionSuffixPattern.MatchString(path.Base(importPath)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 warning

Two nits on the widened match:

  1. ^v[0-9]+$ also matches v0, v01, etc., which are not valid Go major-version suffixes. If someone configures github.com/acme/acme-go/v0 and releases v2.0.0, we now silently emit an unbuildable module path instead of appending /v2. Tighten to ^v[1-9][0-9]*$.
  2. When the existing suffix disagrees with the release major (the /v46 + v34 case), we silently prefer the configured value. That's the right default, but a warn-level log would save the next person a debugging session.
Suggested change
// majorVersionSuffixPattern matches a Go major version suffix, e.g. "v2".
var majorVersionSuffixPattern = regexp.MustCompile(`^v[0-9]+$`)
// maybeAppendVersionSuffix appends the given version suffix to the importPath,
// unless the importPath already ends in a major version suffix. The configured
// suffix wins, even if it doesn't match the version being released.
func maybeAppendVersionSuffix(importPath string, version string) string {
if path.Base(importPath) == version {
if majorVersionSuffixPattern.MatchString(path.Base(importPath)) {
// majorVersionSuffixPattern matches a Go major version suffix, e.g. "v2".
var majorVersionSuffixPattern = regexp.MustCompile(`^v[1-9][0-9]*$`)
// maybeAppendVersionSuffix appends the given version suffix to the importPath,
// unless the importPath already ends in a major version suffix. The configured
// suffix wins, even if it doesn't match the version being released.
func maybeAppendVersionSuffix(importPath string, version string) string {
if majorVersionSuffixPattern.MatchString(path.Base(importPath)) {

const DEFAULT_MODULE_PATH = "sdk";

// Matches a Go major version suffix, e.g. "v2".
const MAJOR_VERSION_SUFFIX_PATTERN = /^v\d+$/;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 warning

Same as the Go side: ^v\d+$ matches v0 and v01, neither of which is a legal Go major-version suffix. A configured .../v0 would now suppress a legitimate /v2 append. Suggest ^v[1-9]\d*$ to keep both implementations aligned.

Suggested change
const MAJOR_VERSION_SUFFIX_PATTERN = /^v\d+$/;
const MAJOR_VERSION_SUFFIX_PATTERN = /^v[1-9]\d*$/;

Comment on lines +505 to +508
expect(result?.type).toBe("object");
if (result?.type === "object") {
expect(result.properties.logo?.type).not.toBe("map");
expect(result.properties.logo?.type).not.toBe("object");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 suggestion

These assertions pass vacuously if logo is dropped from the example entirely (undefined?.type is undefined, which is neither "map" nor "object"). Assert the positive case so the test actually guards the fix.

Suggested change
expect(result?.type).toBe("object");
if (result?.type === "object") {
expect(result.properties.logo?.type).not.toBe("map");
expect(result.properties.logo?.type).not.toBe("object");
expect(result?.type).toBe("object");
if (result?.type === "object") {
expect(result.properties.logo?.type).toBe("null");
}

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 1 additional finding.

Open in Devin Review

@github-actions

Copy link
Copy Markdown
Contributor

Docs Generation Benchmark Results

Comparing PR branch against median of 5 nightly run(s) on main (latest: 2026-08-14T04:33:13Z).

Fixture main PR Delta
docs 268.5s (n=5) 251.8s (35 versions) -16.7s (-6.2%)

Docs generation runs fern generate --docs --preview end-to-end against the benchmark fixture with 35 API versions (each version: markdown processing + OpenAPI-to-IR + FDR upload).
Delta is computed against the nightly baseline on main.
Baseline from nightly run(s) on main (latest: 2026-08-14T04:33:13Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-08-14 21:15 UTC

@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-08-14T04:33:13Z).

Full benchmark table (click to expand)
Generator Spec main (generator) main (E2E) PR (generator) Delta
csharp-sdk square 98s (n=5) N/A 65s -33s (-33.7%)
go-sdk square 145s (n=5) 297s (n=5) 126s -19s (-13.1%)
java-sdk square 221s (n=5) 286s (n=5) 201s -20s (-9.0%)
php-sdk square 78s (n=5) N/A 54s -24s (-30.8%)
python-sdk square 152s (n=5) 256s (n=5) 138s -14s (-9.2%)
ruby-sdk-v2 square 110s (n=5) 152s (n=5) 86s -24s (-21.8%)
rust-sdk square 206s (n=5) 212s (n=5) 193s -13s (-6.3%)
swift-sdk square 78s (n=5) 447s (n=5) 48s -30s (-38.5%)
ts-sdk square 185s (n=5) 185s (n=5) 118s -67s (-36.2%)

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-08-14T04:33:13Z). Trigger benchmark-baseline to refresh.
Last updated: 2026-08-14 21:17 UTC

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.

0 participants