Summary
speakeasy run emits non-deterministic key ordering for nested objects supplied via additionalPackageJSON in the generated package.json. Re-running the generator against an unchanged spec and unchanged gen.yaml flips the key order of nested objects (repository, engines, pnpm.overrides, …) on a subset of runs. This defeats byte-level idempotency / "regen-survival" checks in CI and produces noisy, meaningless diffs on every regeneration.
- CLI version:
speakeasy 1.763.1 (darwin_arm64)
- Target:
typescript
- Scope: confined to nested objects originating from
gen.yaml typescript.additionalPackageJSON. Speakeasy-native top-level keys are stably ordered.
Repro
gen.yaml (excerpt):
typescript:
additionalPackageJSON:
engines:
node: '>=22'
pnpm: '>=10'
repository:
type: git
url: git+https://github.com/example/sdk-js.git
pnpm:
overrides:
vite: ^8
postcss: '>=8.5.10'
Run repeatedly:
for i in $(seq 6); do
speakeasy run --skip-compile --skip-upload-spec --skip-versioning >/dev/null 2>&1
python3 -c "import json;p=json.load(open('package.json'));print('repository:',list(p['repository']),'pnpm.overrides:',list(p['pnpm']['overrides']))"
done
Observed (same spec, same gen.yaml, no edits between runs):
repository: ['url', 'type'] pnpm.overrides: ['postcss', 'vite']
repository: ['type', 'url'] pnpm.overrides: ['postcss', 'vite']
repository: ['url', 'type'] pnpm.overrides: ['postcss', 'vite']
repository: ['url', 'type'] pnpm.overrides: ['vite', 'postcss']
repository: ['type', 'url'] pnpm.overrides: ['postcss', 'vite']
repository: ['type', 'url'] pnpm.overrides: ['postcss', 'vite']
Which specific object flips varies per process invocation; the order is unsorted and variable (not alphabetical, not insertion order).
Likely cause
The order is variable rather than sorted, which rules out Go stdlib encoding/json map marshaling (it sorts map keys deterministically). Combined with the defect being confined to additionalPackageJSON-sourced nested objects while Speakeasy-native top-level keys stay stable, this points at the additionalPackageJSON merge path decoding nested objects into an unordered map[string]any and emitting them via Speakeasy's ordered JSON writer without preserving YAML insertion order or sorting. (gopkg.in/yaml.v3 can preserve order via the Node/MapSlice API; encoding/json would at least sort — neither would produce this variable order, so it appears to be in Speakeasy's own merge/emit, not a dependency.)
Impact
- Byte-level regen-survival / idempotency CI gates false-positive on every regeneration.
- Spec-driven regens produce churny
package.json diffs unrelated to any actual API change, obscuring real changes in review.
Expected
Nested objects from additionalPackageJSON should serialize with stable, deterministic key order across runs (preserving the order declared in gen.yaml, or failing that, a deterministic sort).
Workaround
Assert package.json invariants by value rather than byte-identity in CI.
Summary
speakeasy runemits non-deterministic key ordering for nested objects supplied viaadditionalPackageJSONin the generatedpackage.json. Re-running the generator against an unchanged spec and unchangedgen.yamlflips the key order of nested objects (repository,engines,pnpm.overrides, …) on a subset of runs. This defeats byte-level idempotency / "regen-survival" checks in CI and produces noisy, meaningless diffs on every regeneration.speakeasy 1.763.1(darwin_arm64)typescriptgen.yamltypescript.additionalPackageJSON. Speakeasy-native top-level keys are stably ordered.Repro
gen.yaml(excerpt):Run repeatedly:
Observed (same spec, same gen.yaml, no edits between runs):
Which specific object flips varies per process invocation; the order is unsorted and variable (not alphabetical, not insertion order).
Likely cause
The order is variable rather than sorted, which rules out Go stdlib
encoding/jsonmap marshaling (it sorts map keys deterministically). Combined with the defect being confined toadditionalPackageJSON-sourced nested objects while Speakeasy-native top-level keys stay stable, this points at theadditionalPackageJSONmerge path decoding nested objects into an unorderedmap[string]anyand emitting them via Speakeasy's ordered JSON writer without preserving YAML insertion order or sorting. (gopkg.in/yaml.v3can preserve order via the Node/MapSliceAPI;encoding/jsonwould at least sort — neither would produce this variable order, so it appears to be in Speakeasy's own merge/emit, not a dependency.)Impact
package.jsondiffs unrelated to any actual API change, obscuring real changes in review.Expected
Nested objects from
additionalPackageJSONshould serialize with stable, deterministic key order across runs (preserving the order declared ingen.yaml, or failing that, a deterministic sort).Workaround
Assert package.json invariants by value rather than byte-identity in CI.