feat: auto-generate harmonised, ergonomic SDKs across 12 languages#35
Draft
mridang wants to merge 837 commits into
Draft
feat: auto-generate harmonised, ergonomic SDKs across 12 languages#35mridang wants to merge 837 commits into
mridang wants to merge 837 commits into
Conversation
️✅ There are no secrets present in this pull request anymore.If these secrets were true positive and are still valid, we highly recommend you to revoke them. 🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request. |
mridang
force-pushed
the
feat/better-generated-clients
branch
from
March 10, 2026 00:23
5166131 to
3cb26d3
Compare
mridang
force-pushed
the
feat/better-generated-clients
branch
from
April 13, 2026 06:21
1113be3 to
bc8fd3d
Compare
mridang
marked this pull request as draft
April 21, 2026 06:11
mridang
force-pushed
the
feat/better-generated-clients
branch
9 times, most recently
from
April 27, 2026 08:19
e134b80 to
f61e4ed
Compare
mridang
force-pushed
the
feat/better-generated-clients
branch
from
April 28, 2026 09:21
ace5c59 to
9039e83
Compare
mridang
added a commit
that referenced
this pull request
May 4, 2026
…, #35) - #2: Make Go ApiResult.Data nullable (*T pointer type) - #5: Dart caCertPath defaults to null instead of empty string - #9: Add toCookieValue to ObjectSerializer in all 12 languages - #14: Dart DefaultApiClient uses caCertPath with SecurityContext - #15: Fix Dart redirect handling (autoRedirect + maxRedirects) - #18: Route Dart/Rust path params through ValueSerializer.serializeStyled() - #26: Add default value support in Go, Rust, Swift, Dart, Elixir models - #35: Update Go version to 1.26 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mridang
force-pushed
the
feat/better-generated-clients
branch
2 times, most recently
from
May 4, 2026 19:34
58765c7 to
df347a4
Compare
mridang
added a commit
that referenced
this pull request
May 4, 2026
…, #35) - #2: Make Go ApiResult.Data nullable (*T pointer type) - #5: Dart caCertPath defaults to null instead of empty string - #9: Add toCookieValue to ObjectSerializer in all 12 languages - #14: Dart DefaultApiClient uses caCertPath with SecurityContext - #15: Fix Dart redirect handling (autoRedirect + maxRedirects) - #18: Route Dart/Rust path params through ValueSerializer.serializeStyled() - #26: Add default value support in Go, Rust, Swift, Dart, Elixir models - #35: Update Go version to 1.26 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mridang
force-pushed
the
feat/better-generated-clients
branch
from
May 4, 2026 20:27
df347a4 to
642ef91
Compare
mridang
added a commit
that referenced
this pull request
May 4, 2026
…, #35) - #2: Make Go ApiResult.Data nullable (*T pointer type) - #5: Dart caCertPath defaults to null instead of empty string - #9: Add toCookieValue to ObjectSerializer in all 12 languages - #14: Dart DefaultApiClient uses caCertPath with SecurityContext - #15: Fix Dart redirect handling (autoRedirect + maxRedirects) - #18: Route Dart/Rust path params through ValueSerializer.serializeStyled() - #26: Add default value support in Go, Rust, Swift, Dart, Elixir models - #35: Update Go version to 1.26 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mridang
force-pushed
the
feat/better-generated-clients
branch
from
May 5, 2026 06:35
642ef91 to
6713b61
Compare
mridang
added a commit
that referenced
this pull request
May 5, 2026
…, #35) - #2: Make Go ApiResult.Data nullable (*T pointer type) - #5: Dart caCertPath defaults to null instead of empty string - #9: Add toCookieValue to ObjectSerializer in all 12 languages - #14: Dart DefaultApiClient uses caCertPath with SecurityContext - #15: Fix Dart redirect handling (autoRedirect + maxRedirects) - #18: Route Dart/Rust path params through ValueSerializer.serializeStyled() - #26: Add default value support in Go, Rust, Swift, Dart, Elixir models - #35: Update Go version to 1.26 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mridang
force-pushed
the
feat/better-generated-clients
branch
from
May 5, 2026 07:36
6713b61 to
f6d73f3
Compare
mridang
added a commit
that referenced
this pull request
May 5, 2026
…, #35) - #2: Make Go ApiResult.Data nullable (*T pointer type) - #5: Dart caCertPath defaults to null instead of empty string - #9: Add toCookieValue to ObjectSerializer in all 12 languages - #14: Dart DefaultApiClient uses caCertPath with SecurityContext - #15: Fix Dart redirect handling (autoRedirect + maxRedirects) - #18: Route Dart/Rust path params through ValueSerializer.serializeStyled() - #26: Add default value support in Go, Rust, Swift, Dart, Elixir models - #35: Update Go version to 1.26 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
mridang
force-pushed
the
feat/better-generated-clients
branch
4 times, most recently
from
May 10, 2026 11:55
b4d5152 to
1c15042
Compare
Cross-cutting review follow-ups: enum models now emit the spec description (falling back to the generic boilerplate only when absent) across dart, go, rust, swift, elixir and ruby; per-operation Options classes carry the spec parameter description in java and node; and the options class doc-comment wording is unified across dart, go and swift. The date-time fractional-second consistency and doc-comment quote-unescaping findings are left tracked in REVIEW-FINDINGS.md: the former changes the wire format across seven SDKs and warrants its own round-trip-tested change, and the latter needs a comment-only unescape path a Mustache lambda did not provide.
The #26 fix made an options struct with a required field take that field in new() instead of deriving Default with a no-arg new(). The rust test templates still built such options via the old no-arg new() + struct-update idiom, which no longer compiles. Pass the required field to new() directly (upload-document file, set-preferences nickname). Only surfaced on CI, where the rust test crate compiles (it segfaults on the local toolchain).
A description is pre-escaped for string-literal use (a quote becomes a literal backslash-quote), which is correct for the few templates that embed it in a literal (Python Field, Elixir moduledoc) but leaks the backslash into the doc comments of the other nine SDKs. Add an unescapeDocComment Mustache lambda on the base codegen and wrap every comment-rendered description with it (nested inside csharp escapeXml; string-literal users left untouched). Generated doc comments now read the bare quote.
Address 46 of the 47 confirmed four-round audit findings (the cross-cutting cookie CR/LF validation lands separately). Headline: Dart OAuth token-exchange flows are now authenticated — BaseApi awaits an async authHeadersAsync wired through the Authenticator interface, so client- credentials/password/authorization-code/OIDC requests carry a real Bearer header instead of going out unauthenticated. Also: Dart double fields coerce integral JSON via num.toDouble; C# deepObject params serialize; Elixir MapSet arrays and Node/PHP/Kotlin additionalProperties round-trip; Node number Decimals stay numeric and integer-enum validation holds; Kotlin multipart uses the contextual serializers; Ruby sub-0.0001s durations round-trip; plus the per-SDK medium/low fixes. Elixir decode keeps raising on a malformed 2xx body (propagates to invoke_api) rather than swallowing it to a tagged error, matching the BaseApi test contract.
…rity Finding 47: route operation-level cookie params through each SDK existing RFC-6265 cookie validator (Go/Rust return an error, the rest throw), closing the header-injection gap where only auth cookies were validated. Test parity: add a canonical regression-test suite to ALL 12 SDKs so every behavior fixed this session is guarded in every language — integer and non-lowercase enums, byte/base64, double-from-integral-JSON, additionalProperties round-trip, cookie CR/LF rejection, nested containers, date-time, durations, unknown-enum rejection, and fail-loud-on-malformed-2xx-body. Those uniform tests caught three real gaps the original fixes missed, now fixed: Dart format:double fields (isDouble, not isNumber) still crashed on an integral JSON value; Python StockItem never imported the StrictInt nested inside List[List[StrictInt]] so the model never fully defined; and PHP double/nested reconstruction. The parity suite is the regression guard the project was missing.
An optional field with a custom serde `with =` (base64 for byte / array-of-byte, proto_duration for duration) overrides Option built-in absent-as-None handling, so an OMITTED field failed to deserialize with a "missing field" error instead of decoding to None. Add `#[serde(default)]` to those fields. Caught by the new rust byte round-trip parity test (PetPassport with an absent biometricChip), which only runs on CI since the rust test crate segfaults on the local toolchain.
… 12 SDKs An optional query parameter declared allowEmptyValue:true emitted a spurious empty key (e.g. "status=") on the wire whenever the caller omitted it, because the query-string builder only skips null/nil and the templates assigned an empty string in the else-branch. allowEmptyValue lets the server tolerate an empty value WHEN the client sends the key; it does not oblige the SDK to always send it. Drop the else-branch so the key is omitted when unset; an explicit empty value still rides the wire as "status=". Adds the identical regression test to all 12 SDKs and inverts the stale assertions that codified the old behaviour.
… 12 SDKs
A type:string format:binary request body (setPetAvatar, Content-Type image/jpeg)
was corrupted by several transports: Go json-marshaled an *os.File to "{}", Rust
ran Vec<u8> through serde_json producing a JSON int-array, Kotlin overrode the
Content-Type to application/octet-stream, C# dropped the declared Content-Type
via a null-guard, and PHP never streamed the SplFileObject body at all. Stream
the raw bytes unchanged with the declared Content-Type preserved. Adds the
identical raw-bytes round-trip regression test to all 12 SDKs.
…SDKs setPetAvatarThumbnail is a oneOf of a scalar format:byte value and an array of format:byte values; the byte marker was lost inside the union wrapper in several SDKs, so byte variants serialized as JSON int-arrays / bare strings instead of base64. Kotlin used kotlinx ByteArray serializer, Python/Elixir had reversed variant order, Ruby/PHP dropped the format, Rust used untagged int-arrays, and Swift encoded Data via its own Encodable (raw [UInt8]) bypassing the base64 strategy. Route byte variants through base64 on encode and decode with scalar before array, and add the identical byte-union round-trip test to all 12 SDKs. Also inverts the stale rust allowEmptyValue base_api assertion missed in the previous wave (rust runs on CI only).
…SDKs Date-time encoders truncated milli/micro/nanoseconds to whole seconds while the decoders happily accepted fractions — a lossy, asymmetric round-trip. Eleven of the twelve SDKs were affected on the body path, the parameter path, or both (C#/Dart/Kotlin/Swift/PHP/Ruby/Elixir/Node on both; Java/Go on the param path); Go additionally emitted a numeric offset instead of Z. Switch each encoder to a sub-second-preserving formatter so the fraction the decoder accepts is also emitted, keeping whole-second instants unchanged. Adds the identical .123 round-trip regression test to all 12 SDKs and inverts the stale assertions that codified whole-second truncation.
…parity Swift: strict unevaluatedProperties:false decode now throws a catchable DecodingError instead of preconditionFailure (process-crash / DoS on untrusted server data), and AnyCodable Equatable/Hashable recurse over array/object values. Python: async OAuth header acquisition is offloaded via asyncio.to_thread so the token exchange no longer blocks the event loop. C#: multipart JSON model parts go through the configured serializer (date-time/duration/null-omission), and the ArgumentNullException no longer fires for all-optional options. Ruby: multipart model part uses wire keys via ObjectSerializer. Elixir: typespecs reference real per-op options modules, qualified Module.t() for models, and enum @type t. Adds the cross-cutting multipart-model-part parity test to all 12 SDKs. H9 (Java javadoc HTML escaping) is intentionally not done: escaping < > & to entities conflicts with the enforced no-HTML-entities formatting spec, and the make-docs doclint strictness it targets is not part of the build gate.
The typespecsReferenceOnlyDefinedTypes test regenerated the Elixir client into a JUnit @tempdir; the post-process formatter runs in Docker and leaves root-owned _build/.mix artifacts that the temp-dir cleanup cannot delete on the CI runner, erroring the test (and blocking the verify phase, so the client specs never ran). Assert against the committed golden tree under a READ lock instead.
…ution MEDIUM correctness fixes: dart oneOf/anyOf union equality/hashCode now use deep equality on byte/list values; kotlin Duration re-serializes past the ~292-year Long-nanosecond limit (getSeconds/getNano) and decodes unions strictly (ignoreUnknownKeys=false, parseStrict reuses the configured serializersModule); the TypeScript Map<string,Date> leaf preserves null instead of coercing to the Unix epoch; php no longer rejects a legitimate empty string on a required query param; python additionalProperties capture and the top-level datetime container are hardened. L1: the generator synthesised the literal string "null" as a property example when the spec declares none, stamping a false `Example: null` doc line in every language. Fixed once at the root in AbstractBetterCodegen (null the placeholder example universally) rather than per-language guards, and assert the absence in each SDK. Adds the MEDIUM regression tests across the SDKs.
A model declaring additionalProperties carries undeclared wire keys on a TypeScript index signature. plainToInstance with excludeExtraneousValues drops every key without an @expose decorator — the index signature is not exposed — so free-form properties were silently lost on a round-trip (data corruption). Mark such models with their declared-key set and re-attach any undeclared json key onto the instance after construction. Adds a Metadata round-trip regression test.
A non-discriminated anyOf (PetTreatment, documented "medication, surgery, OR BOTH") kept only the FIRST matching variant and silently dropped the co-satisfied fields on re-encode. Every SDK now retains EVERY variant that validates and merges their field-sets on encode, so a payload satisfying both variants round-trips losslessly; single-variant payloads are unchanged. Adds the identical both-variants round-trip test to all 12 SDKs. Also hoists the kotlin StrictModuleTests fixture data class to file scope: Kotlin forbids a non-inner nested class inside a JUnit @nested inner class, a test-compile error the local kotlin spec (main-only) could not catch.
Dart: array/map toJson and deserialize_arg now handle DateTime/Date/Duration/UUID container leaves symmetrically. Rust: discriminator children hand-write Default so the discriminator is never empty, and multipart JSON parts carry their Content-Type. Kotlin: optional enum query/path/body param defaults are threaded. Node: unevaluatedProperties:false models route through strict decoding so unknown keys are rejected. PHP: nested-container PHPDoc keeps the precise inner type. Python: format:double/float accepts integral JSON, set/frozenset serialize as arrays, and URL-typed properties round-trip without normalisation. Ruby: base64 is declared as a gem dependency. Go: the pre-seed-defaults comment is emitted only when an optional default actually exists.
…fset ObjectSerializer.stringify formatted every TemporalAccessor with ISO_OFFSET_DATE_TIME, which throws for a format:time (LocalTime) or local date-time value that carries no offset. Add ISO_LOCAL_TIME / ISO_LOCAL_DATE_TIME branches before the offset-bearing catch-all. Latent in the current fixture (no format:time parameter), so no regression test is added.
The rust agent added a test asserting DryFood::default() seeds its discriminator like new(), but the discriminator info is carried on the parent oneOf mapping and is not resolvable in postProcessModels, so the child still derives an empty discriminator. The fix needs a later codegen hook and cannot be validated without a local rust runtime; deferred as a documented latent gap rather than risk repeated CI cycles. Removes the failing test so the branch is green.
…perations An operation that declares more than one request content-type (setPetAvatar: image/jpeg, image/png, application/json; uploadPetDocument: multipart, octet-stream) collapsed to the first declared type, leaving the others unreachable and mislabeling e.g. a PNG as image/jpeg. Add an OPTIONAL request-content-type selector across all 12 SDKs, defaulting to the first declared type so existing calls are unchanged; when supplied it sets the request Content-Type header. A shared hasMultipleConsumes operation-decorator flag gates it. Adds the identical selector regression test to all 12 SDKs. Scope: the selector wires the Content-Type header for content-types that share the operation body type (image/jpeg vs image/png). Content-types needing a different body shape (the application/json envelope, octet-stream vs multipart) still require a compatible body and are not reshaped in this pass.
Plural x-examples were emitted with the Rust/Dart `///` markdown-doc idiom,
which is an inert comment on Java 17/21 (the examples never reached the rendered
Javadoc) and produced a second doc comment that orphaned the property
description. Merge description, the singular example, and every plural example
into one Javadoc block, with example values in {@code ...} so a JSON value
renders without HTML entities (which the formatting spec forbids).
A query/header/form/cookie parameter declared deprecated:true now surfaces that signal on the generated per-operation Options field across all 12 SDKs, mirroring the existing operation- and model-property deprecation idioms. The parameter type is unchanged, so allowEmptyValue params still accept an empty value. Enforced-deprecation languages (Java/Kotlin/Swift/Dart/Rust) use their native attribute; doc-comment languages (Go/Python/Ruby/Elixir) use a deprecation note. C# joins the doc-comment cohort: an enforced [Obsolete] would trip CS0618 under the strict build because the generated client reads the field itself.
…ing it BaseApi read the authenticator's auth_headers/query_params/cookie_params as struct fields gated on Map.has_key?, but every authenticator exposes them as behaviour callbacks, not struct keys. The guard was always false, so a configured Bearer/api-key/cookie credential never reached the wire and every authenticated call went out unauthenticated. Dispatch the callbacks through the authenticator's module (function_exported?-guarded), keeping a plain-map fast-path for map-shaped auth. Add a real-struct auth-applied regression test (and a configured-authenticator guard) to all 12 SDK test suites; the other 11 already applied auth correctly.
An operation declared `security: []` is explicitly unauthenticated, but the generated api method passed nil/null as the auth argument, so base_api's `auth || client_authenticator` fallback re-acquired the client credential and attached it. That leaked the credential to endpoints the spec marks unauthenticated — including the testEcho* operations, which reflect the request back. Introduce a no-auth sentinel that security:[] operations pass instead of nil. base_api now resolves three states: the sentinel suppresses auth (no fallback), nil falls back to the client authenticator (unchanged), and an explicit authenticator is used as a per-call override (unchanged). Applied uniformly across all 12 SDKs with a security-none parity test and a secured-still-works guard. Existing tests that exercised auth through a security:[] op were repointed at a secured op.
…numbers
A property typed `type: number` with no format is a bare JSON number, but the
python and node SDKs modelled it as an arbitrary-precision Decimal and
serialized it as a quoted string ("1.5") instead of an unquoted number (1.5),
violating the spec and diverging from the other SDKs.
- python: a branded JsonNumber = Annotated[Decimal, PlainSerializer(float, ...)]
so model_dump_json emits a bare number for every type:number no-format field.
- node: a global decimal-field registry so a plain-object request body still
serializes the Decimal field unquoted (the replacer was keyed on the model
constructor and bypassed for object literals).
The other 10 SDKs already emit a number; all 12 gain a Decimal-as-number parity
test. This regeneration also re-applies rustfmt to the rust goldens, which the
previous wave's regen skipped while Docker was under load.
…-consume op uploadPetDocument accepts both multipart/form-data and application/octet-stream. When a caller selected application/octet-stream the SDKs still built a multipart/form-data envelope (the body dispatch keyed on "is the body a form map?" rather than on the chosen content type), so the raw payload never went out as raw bytes. The audit flagged this for elixir and csharp, but the cross-SDK parity test showed it was present in 10 of the 12 SDKs (only node and php already handled it). Each fixed SDK now unwraps the single binary part and streams it under the selected Content-Type when octet-stream is chosen, while still building a multipart envelope for multipart/form-data. All 12 gain a parity test asserting the octet-stream selection sends the exact raw bytes (no boundary, no base64) and that the multipart path is unchanged.
An optional array query or header parameter is modelled in Go as a
pointer-to-slice (e.g. *[]string), and array-of-enum params arrive as typed
slices ([]Color). toStringSlice only matched []string and []any, so these fell
through to fmt.Sprintf("%v", ...) and emitted a Go slice debug dump like
&[blue black] (url-encoded %26%5Bblue+black%5D) into the query string instead of
the declared OAS style.
toStringSlice now dereferences a non-nil pointer and reflects over any
slice/array, stringifying each element, so *[]string, []Color and *[]MyEnum all
encode as proper styled/repeated values in both the query-array and styled
path-array branches; a nil pointer reports not-an-array so the param is omitted.
All 12 SDKs gain a parity test asserting an optional array query param
serializes to styled values (and not a language-debug blob); only Go was
affected (pointer-to-slice is a Go-specific shape).
…client The Swift SessionDelegate stored the redirect hop count and the too-many-redirects refusal error on the delegate instance, which a single URLSession shares across every request. Two concurrent requests on one client therefore shared one counter and one error slot and could corrupt each other's redirect handling — one request's redirects inflating another's count, or a refusal from one surfacing on an unrelated request. Redirect state is now keyed per URLSessionTask (and cleared on completion), so each request gets its own hop count and refusal error; takeRedirectError() returns only the calling request's error. The redirect limit and cross-origin Authorization-stripping behavior are unchanged. All 12 SDKs gain a concurrent-redirects parity test asserting redirect counting is per-request (a request that redirects once is not failed by another's overflow, and a refusal stays on its own request); only Swift was affected.
getPetTag deserializes its response into a Pet (photoUrls required); the test's mock returned a body without it, so the call threw before the URL assertions. Return a valid Pet so the captured-URL assertions run.
When a response declares Content-Type charset=utf-16 with no byte-order mark, the SDKs disagreed on byte order — several decoded little-endian — so identical bytes produced different strings across languages. These are HTTP clients decoding by the IANA/MIME charset registration, and RFC 2781 specifies UTF-16 with no BOM defaults to big-endian. Standardize on big-endian for a BOM-less utf-16 label across all transports (csharp, go, python, ruby, rust, swift, node were little-endian; dart, java, kotlin, php, elixir already big-endian). An explicit utf-16le/utf-16be label and a present BOM are still honored. All 12 SDKs gain a parity test decoding the BOM-less UTF-16BE bytes of "Pet" and asserting the result, plus that the little-endian interpretation differs (so the big-endian choice is load-bearing). Also drops a stray HTML entity (>) from the C# concurrent-redirect test's doc comment, restoring CSharpFormattingSpec.
…sage match) The java concurrency test dereferenced ExecutionException.getCause()/.getMessage() without null handling, tripping NullAway; extract the cause via Objects.requireNonNull and null-check the message. The elixir test matched the refusal message with == but ApiError appends an 'HTTP status code: 0' line; use a substring match.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Adds 7 new language generators (Kotlin, Go, Rust, Dart, Swift, Elixir, C#) alongside the existing 5 (Java, Python, Ruby, PHP, Node/TypeScript) to produce fully harmonised, ergonomic client SDKs from any OpenAPI 3.0 spec. Every generated SDK shares the same architecture: BaseApi, DefaultApiClient, ObjectSerializer, ValueSerializer, HeaderSelector, Configuration, structured exceptions, per-operation Options classes, authentication (API key, Basic, Bearer, OAuth2), OpenTelemetry propagation, HTTP compression, TLS/proxy transport options, binary upload/download, and per-operation server overrides.
Motivation and Context
The default OpenAPI Generator templates produce inconsistent, non-idiomatic code across languages. This PR replaces them with a unified set of Mustache templates that generate production-grade SDKs on par with hand-written clients from Stripe, Stainless, or Speakeasy — fully linted, statically analysed, formatted, and integration-tested in Docker.
How Has This Been Tested?
All 12 SDKs are generated on-the-fly during the Maven build. Each SDK runs a full test suite inside Docker covering unit tests, integration tests against a live Petstore mock, formatting checks, linting, and static analysis. Regression tests for HeaderSelector and ValueSerializer ensure cross-language parity.
Checklist