NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
pub.dev · #1805 most downloaded on pub.dev
An OpenAPI generator, focused on generating high quality code.
Last release 2 months ago
19 Jul 2026
Release timing varies
gaps range from 2 weeks to 8 months
Nearly every release is documented
notes for 8 of 8 stable releases
Nothing withdrawn
no release was ever pulled
1 years old
8 releases · first in 2025
One column per month.
Correctness pass over the optional/nullable/default matrix, broader real-world spec support (split-spec external refs, nested allOf, cookie API keys,
Correctness pass over the optional/nullable/default matrix, broader
real-world spec support (split-spec external refs, nested allOf,
cookie API keys, form-urlencoded bodies, lone-scalar const), and
precise stale-file handling when regenerating a package in place. Generated output also got
markedly cleaner: a long refactor arc made the generator emit const
constructors and instances, int literals, tearoffs, and tightly
scoped imports, cutting the raw dart fix lint count on the github
regen by roughly an order of magnitude.
toJson made every property a key, so
an optional property the caller never set went out as {"x": null}
— an invalid payload for a non-nullable type — and fromJson
applied defaults with ??, which fires on an explicit null as well
as a missing key, contrary to JSON Schema (default applies to
absence). Fixed across fromJson, storage, and toJson for the
whole matrix (#321, closes #318, supersedes #102/#316).$refs — the
one-component-per-file convention (redocly/stoplight, openfoodfacts).
Fragment-less whole-file refs, arbitrary JSON-pointer targets into
documents with no components: section, and aliases all resolve now,
where before any of them threw has no components: section and
generation never started (#319, #315).allOf member instead of crashing. An allOf
whose member is itself an allOf — a common composition shape —
threw allOf only supports objects. The nested member is
object-shaped like the others, so it now folds into the parent class
and the existing recursive merge does the rest (#322, #320).const property as a fixed getter. A
property pinned to a single value ({type: string, const: "list"},
common for envelope markers like Stripe's object: {const: "list"}
and version pins) was modeled as a throwaway single-value enum,
minting a class and file per const. It now renders as a plain getter
returning the literal — no constructor argument, absent from
fromJson and equality, toJson emits the literal directly (#317,
#240).apiKey security schemes with in: cookie. They threw
UnimplementedError at parse time; the generated client now sends
the key as a Cookie: header, alongside the already-handled
in: header and in: query (#314).application/x-www-form-urlencoded request bodies. The
body renders as a Map<String, String> that http encodes as
key=value&… form fields, with optional fields behind a
collection-if and non-String scalars coerced. Form-urlencoded is
lowest in content-type precedence, so a body that also offers JSON
still picks JSON; it wins only as the sole content type. No existing
generated operation changes (#325).test/ were
never cleaned at all. FileRenderer.generatedDirs now declares the
directories the generator owns; those are emptied each run and
nothing else is touched. Generated tests move to test/gen/ so the
cleared directories are ones we unambiguously own, and --no-clear
(GeneratorConfig.clearDirectory: false) opts a shared package out
entirely (#313, #310, #305, #300).comment_references. The file-wide
ignore_for_file was added whenever a doc comment held any bracketed
token, including ones the analyzer never treats as a reference —
GitHub-flavored-Markdown alerts ([!NOTE], [!WARNING]) copied from
spec prose, quoted enum values, bare numbers. Of 43 annotated files
on github, 27 were annotated for tokens the lint would never fire on;
now only real comment-reference shapes count (#312).allListsDefaultToEmpty quirk an optional array query parameter got
= const [], making it non-optional in practice and emitting a no-op
if (x != null) guard that always fired. The quirk now applies only
to response-model fields, where it belongs; an optional array
parameter defaults to null, matching the non-quirk output (#326,
#137).format: date and base64 properties in a multipart body.
Both crashed the generator with multipart/form-data property must be a scalar or binary; their wire value is a plain String, so they
render as text fields (#297, #294).format: byte as base64 bytes. An OAS 3.0 spec now
gets the same Uint8List a 3.1 contentEncoding: base64 spec does,
instead of a String the caller has to decode by hand (#303, #298)..map(MapEntry.new) that only copied it (#301, #272).Uri field by field. Fixes
ApiClient._resolveUri losing the operation path when baseUri
carries a query string, collapsing a trailing slash into an empty
// segment, and closes a query-injection hole in path parameters
(#279).SCREAMING_CAPS property names snaked character-by-character
(WorkflowUsageBillableMACOS → ..._m_a_c_o_s); now
..._macos (#288).directives_ordering wants, fixing 145
duplicate directives and api_client.dart tripping the lint for any
package whose name sorts before http (#275, #286).?. on a
non-nullable receiver, which raised invalid_null_aware_operator in
generated models (#277).Two more real-world specs now generate analyze-clean Dart with a fully passing generated round-trip suite: Discord (~511 schemas; 6 genuine multi-vari
Two more real-world specs now generate analyze-clean Dart with a fully passing generated round-trip suite: Discord (~511 schemas; 6 genuine multi-variant-union stubs remain) and Backstage's catalog API (zero stubs).
Date value type for format: date instead
of DateTime. A DateTime is a specific instant, so a calendar
date backed by one leaks timezone bugs no normalization can close:
UTC midnight reads as the previous day under .toLocal() in
negative-offset zones, and local midnight breaks the wire round-trip
and hits the DST no-midnight gap. The generated Date holds
year/month/day only and forces an explicit timezone to convert
(toUtcDateTime / toLocalDateTime). It's emitted once as
lib/date.dart, pruned when no date field is present, and
re-exported from the barrel. Public-API change: format: date
fields are now Date instead of DateTime, and a pure-format: date named schema collapses to Date rather than minting a
redundant per-schema newtype (#229).explode: false on query array parameters. Comma-joins an
array into a single value (?tags=a,b,c) instead of the default
explode: true repeated-key form (?tags=a&tags=b&tags=c) — those
are genuinely different wire formats a server won't silently
normalize, so ignoring explode: false was a real correctness bug.
allowReserved stays deliberately unhonored (with a comment on why);
the WARN keeps any remaining divergence visible (#232).oneOf: [{type: null}, T] to a plain nullable T?. Each previously resolved to an
undispatchable sealed union whose fromJson couldn't dispatch on
null, emitting an UnimplementedError stub — Discord spells
nullable this way ~290 times. A genuine 3+ member union
(oneOf: [null, A, B]) still renders as a union, now marked nullable
(#234).$ref-to-enum pinned to a
const — the OpenAPI-3.0 idiom {allOf: [{$ref: E}], enum: [v]},
meaning "a value of enum E fixed to one member," where the field's
type is the shared enum and the pinned value is the discriminator.
The parser previously dropped the pinning enum when it saw allOf,
leaving every variant's tag indistinguishable and emitting an
UnimplementedError stub (#236).allOf into the object's
overflow. allOf: [<JsonObject>, <object>] — the
TypeScript-intersection idiom "these known named fields plus an
arbitrary bag of extra keys" — previously crashed with
FormatException: allOf only supports objects (#219).properties: {} alongside additionalProperties now
routes to the map path. It was falling through to a lossy
SchemaEmptyObject stub whose fromJson discarded its input and
whose toJson always emitted {}, so every entry silently
round-tripped away. It's now treated identically to the
omitted-properties map form (#218).operator[] indexes the whole object, not just the
additionalProperties overflow. x['name'] returned null for a
real named key that toJson() emits — a map-style accessor that
disagreed with the object's own serialization (#221).List<Uri> / List<DateTime> via .map((e) => parse(e)),
not a broken .cast<Uri>(). The cast produced a lazy view that
threw type 'String' is not a subtype of type 'Uri' on first element
access; these fields serialized fine but blew up on decode (#244).unintended_html_in_doc_comment for spec-prose angle
brackets — but not when they're inside a code span or fenced block.
Spec descriptions copy prose verbatim into dartdoc, where a token
like <sha1hex> reads as an HTML tag start and trips
very_good_analysis's lint on every consumer package. The
suppression is scoped to genuine cases: <...> inside backticks
(`Map<String, dynamic>`) or a fenced sample is rendered
verbatim by dartdoc and no longer triggers it (#215, #246).toString() == toJson() assertion is a category error
for int enums ("1" vs 1) and accounted for 66 of Discord's 67
generated-test failures; it's now gated to string enums, with a
type-safe assertion restoring int-enum toString() coverage
(#241, #243).body.contains(name) matched maybeParseDate inside
maybeParseDateTime (and maybeParseUri inside
maybeParseUriTemplate), writing an uncalled helper into
model_helpers.dart that read as uncovered lines on the generated
package (#211).`lines_longer_than_80_chars` suppression now scopes to files space_gen actually emits and matches the analyzer's own carve-outs. The previous post-wal
lines_longer_than_80_chars suppression now scopes to files
space_gen actually emits and matches the analyzer's own carve-outs.
The previous post-walk pass scanned every .dart file in the output
directory — including hand-written templates a FileRenderer
subclass was deliberately preserving via renderClient /
renderPublicApi no-op overrides. It also counted raw line length
without the analyzer's URI exclusion, so files whose only over-80
lines were import / export statements got the directive added,
then dart fix --apply stripped it as unnecessary_ignore.
Replaced with a per-file emit-time helper threaded through the same
_renderTemplate pattern PR #138 settled on for
comment_references. Skips import / export lines when measuring
(#209).dart format later reflows
long lines under 80, dart fix --apply strips the now-pointless
directive as unnecessary_ignore, and the 3-line justification
comment is left orphaned (61 such files surfaced on a real-world
regen). Add package:dart_style as a dep and run DartFormatter
in-process from _renderTemplate / _renderSpecTemplate before
applying transforms. Format settings come from the consuming
package's pubspec.lock / pubspec.yaml (language version) and
analysis_options.yaml (formatter: page_width,
formatter: trailing_commas), with relative include: directives
followed so workspace setups resolve correctly. Without per-consumer
settings, hard-pinning to latestLanguageVersion would silently flip
generated files to tall style and strip trailing commas from
packages that opt into trailing_commas: preserve. Also drops a
bogus /// carve-out from maybeAddLongLineIgnore (the analyzer
flags long doc comments) and extends maybeAddCommentReferencesIgnore
to resolve against same-file field declarations, not just top-level
types — fixes the same orphan-justification problem on the
comment_references directive. The runtime cost is absorbed: clean
inputs make dart fix --apply slightly faster, and the global
dart format step becomes a near no-op for files we wrote (#210).Formatter (subprocess), and the new
in-process DartFileFormatter move to a dedicated
lib/src/render/formatting.dart. FileRenderer now holds a
DartFileFormatter and calls into it; the formatting concerns
cluster together rather than threading through the renderer
(part of #210).Replace the README's stale TODO list (most items shipped in 1.2.0) with a Tested specs section that records the real-world specs space_gen iterates ag
`oneOf` / `anyOf` dispatch — full sealed-class generation. Previously every oneOf body emitted throw UnimplementedError from the synthesized parent's
oneOf / anyOf dispatch — full sealed-class generation. Previously
every oneOf body emitted throw UnimplementedError from the synthesized
parent's fromJson. The renderer now picks one of five strategies and
emits real Dart 3 pattern-matched dispatch:
discriminator: {propertyName, mapping} switches on json[propertyName] (#143).int/String/bool/Map/List/num) switch on the runtime type;
extends through RenderArray, RenderNumber, and RenderMap variants
(#145, #147, #151, #152).containsKey. A variant with no required fields can act as the
catch-all fallback (#146, #153, #154).oneOfs combine an outer shape
switch with required-field sub-arms inside Map<String, dynamic>,
plus array-element shape sub-arms inside List<dynamic> (#156, #183).anyOf<array<X>, array<Y>> peeks the
first list element to pick a variant (#159).oneOf emitted a per-variant data class plus a wrapper
subclass holding final value: <DataClass>. When a variant's pointer is
exclusive to one parent, the variant data class itself becomes the sealed
subclass, inlined in the parent's .dart file — no wrapper, no value
indirection, pattern matching destructures the variant's fields directly.
Covers predicate-required, discriminator, shape, hybrid, and inline
allOf variants; also extends to top-level $ref variants used by
exactly one parent (#168, #169, #170, #179, #189). Round-trip tests are
synthesized for each smooshed variant (#181). Structurally-identical
oneOf trees synthesized under operation paths share one resolved
schema — one Dart class instead of multiple byte-identical sealed
classes (#180).200: User, 202: AcceptedJob (or 201: Empty, 204: ø) used to coalesce into a discriminator-less RenderOneOf and
throw UnimplementedError. Now emit a sealed parent with one final
wrapper per status code; the api method body switches on
response.statusCode. Empty-body and empty-schema 204s render as
value-less / dynamic-valued wrappers respectively (#148).NameAllocator runs an order-independent fixpoint and disambiguates
remaining collisions with numeric suffixes. Title-derived names land
for inline oneOf/anyOf variants whose spec carries a unique
title:. Wrappers no longer double-prefix
(ProjectsCreateCardRequestProjectsCreateCardRequestOneOf0 →
ProjectsCreateCardRequestVariant0) (#161, #162, #163, #165, #166,
#167).type: integer, enum: [...]) parse and render as
typed enum classes with valueN variant names (value1, valueNeg1)
via a new SchemaEnum<T> family pinning T to String or int (#192).const parses as a single-value enum, equivalent to
enum: [X]. Untyped const infers integer vs string from the
value (#198). A oneOf whose every variant is a single-value const
collapses to one typed enum class — eliminates 80 stub UnimplementedError
factories on Discord-shaped specs (#200).pattern / minimum / maxLength etc. is always valid by
construction; extension type const Foo._(String value) gains a
public constructor body that runs the validators. The synthesized
round-trip test's example value is now schema-aware (regex-tested
candidates, minLength/maxLength-resized) so it satisfies the
schema's own rules. Per-call validatePattern(...) at API call sites
is dropped for newtype params — 327 broken call sites in Discord's
default_api.dart cleared (#194).contentEncoding: base64 on a type: string property renders as
Uint8List with base64.encode / base64.decode applied automatically
at the JSON boundary; nullable cases route through new
maybeBase64Encode / maybeBase64Decode helpers (#201).example: and examples: on parameters and headers thread through
parse → resolve → render and surface in generated dartdoc as
/// [paramName] example: \value`(scalars in backticks; Map/List in fenced ```json ` blocks). Schema examples use the same emission
point (#178).oneOf / anyOf at property schema slots are honored even
when the property also declares type: [null, string, integer] — the
explicit collection takes precedence over the multi-type expansion,
preserving per-variant detail like format: date-time (#174).oneOf / anyOf (every variant
is {required: [...]} with no shape of its own) parse as a plain object
with all properties optional; the xor constraint becomes a runtime
concern (#155).oauth2 and openIdConnect. Both
deliver opaque bearer tokens at the wire level; route them through
HttpSecurityScheme(scheme: 'bearer') and reuse the existing bearer
plumbing. Token acquisition (grants, OIDC discovery, refresh) stays
the caller's responsibility via ApiClient(readSecret: …) (#126).text/plain, text/html,
application/octocat-stream, …) return response.body directly
instead of crashing in jsonDecode. JSON responses are unchanged
(#127).space_gen -i api.github.com.json -o /tmp/api.github.com now succeeds; the package
name lands as api_github_com and the CLI logs the substitution.
Programmatic callers building GeneratorConfig directly still trip
the validatePackageName safety net (#123).space_gen declares its executables: so dart pub global activate space_gen puts a space_gen command on PATH (#118).example/ directory with a minimal petstore spec and CLI walkthrough,
for pub.dev's package-scoring example/ check (#117).dart-lang/setup-dart
reusable workflow with OIDC auth (#116).oneOf, EmptyObject, and
additionalProperties. A nullable oneOf property emitted
as Map<String, dynamic> (non-nullable) and crashed on null round-trip;
RenderEmptyObject hard-coded const $className() on read and
const <String, dynamic>{} on write, dropping inner state; the
additionalProperties for-loop swept named-property keys into the
catch-all entries field. Closes 41 pre-existing failing round-trip
tests on the github regen (#184).type: [T, "null"] plus
the property in required) now route through a new checkedKey helper
that verifies containsKey before reading. Previously the generated
fromJson silently accepted a missing key as null, contradicting the
generated round-trip test (#121).allOf synthesis merges required properties. The
ResolvedAllOf → RenderObject synthesis dropped each member's
requiredProperties lists, silently making required fields nullable
in the constructor and hiding required-property tags from oneOf
dispatch detection (#150).oneOf is a sibling. A
type: object schema with both properties / required AND
oneOf / anyOf siblings used to flip into oneOf-mode and lose the
parent's fields; now merges them into each variant at parse time when
every variant inline-refines the parent (#182).ThreadSearchResponse), the renderer hard-computed
the wrapper class name without going through the naming pass — the
allocator's collision suffix wasn't applied. Wrapper names now consult
the allocator (#196).style=form, explode=true) emits ?key=v1&key=v2; headers
(style=simple, explode=false) comma-join into one value. Previously
every array param shipped List.toString() URL-encoded as
?tags=%5Ba%2C+b%5D (#134). Non-default style / explode /
allowReserved now warn instead of silently producing wrong output
(#141).? on URLs without query params is dropped (Uri.replace
always appends ? even when the merged map is empty) (#140).fromJson for nullable
const-default fields. Previously Foo.fromJson({}).isFavorited == null
even though the spec declared default: false. Also fixes a latent
WaitTimer(null) crash in RenderNumber.defaultValueString (#128).avoid_positional_boolean_parameters via a
per-file // ignore_for_file: directive — the type name is the
disambiguation. Directive carries its own justification block to
satisfy document_ignores (#124, #129).comment_references in generated dartdoc is suppressed via a
per-file directive when a /// line contains [<token>] not followed
by ( (legitimate [Foo](url) markdown links left alone). Decision
happens at emit time, not as a post-walk readback (#130, #138).wrapLines now tracks backtick parity per line and refuses to break
inside an open span. Closes unintended_html_in_doc_comment hits
caused by long backticked descriptions like `heads/<branch name>`
(#125)..App.Features.Marketplace.Order.Foo from
.NET / NSwag-style specs) collapse to underscores in toSnakeCase,
not pass through verbatim — fixes a hard-fail on real specs (#119).quirks.screamingCapsEnums. The quirk's gate previously also covered
property and parameter names, so snake_case spec-side identifiers
like petstore's api_key survived into Dart variables and tripped
non_constant_identifier_names. Enum SCREAMING_CAPS preservation is
unaffected (#122)... cascade chain
(id..validateMaximum(10)..validateMinimum(1)) instead of duplicated
receiver statements, silencing cascade_invocations (#197).model_helpers.dart is no longer a single source of "noisy"
warnings: unknown format warnings demote to detail logs (#131); a
cluster of spurious "unused" warnings on misplaced spec fields
(vendor x-*, maxProperties, misplaced required, webhooks,
externalDocs) is silenced (#176).oneOf dispatch picker lifts off of the render tree and into a
dedicated dispatch.dart phase that operates on the resolved tree.
Dispatch decisions are a sealed DispatchDecision family
(DiscriminatorDispatch, ShapeDispatch, HybridDispatch,
PredicateDispatch, NoDispatch); the if-chain tag-field is a
sealed Predicate IR (KeyExists, ArrayElementHasKey,
PropertyArrayFirstIsType, PropertyArrayItemShape, Always); the
per-mode template-context shape is a sealed _DispatchMode so the
type system enforces "exactly one mode active" (#157, #158, #160).Honor security: [] on an operation as an explicit override of the global security requirement (public endpoint), rather than silently inheriting from
security: [] on an operation as an explicit override of the
global security requirement (public endpoint), rather than silently
inheriting from the spec-level security. Previously the parser
conflated "security key absent" with "security is the empty list",
so any operation marked public under an otherwise-authenticated API
still generated an authRequest: argument pointing at the global
scheme. Covered by a new gen_tests/security.json fixture that
exercises the Shorebird-shaped case: global bearer auth, one endpoint
opting out via security: [], and one endpoint swapping to an
apiKey scheme.multipart/form-data request bodies. Endpoints whose request
body schema is an object of scalar + format: binary properties now
generate a Dart method that takes the body object, unpacks it
inline into text fields and http.MultipartFile.fromBytes file parts,
and sends via a new ApiClient.invokeApiMultipart runtime method
(which builds an http.MultipartRequest with its own Content-Type
boundary). Objects with binary properties correctly throw
UnsupportedError from toJson/fromJson instead of emitting
dead-code-after-throw. Priority when a spec lists both JSON and
multipart for the same body: JSON still wins. Out of scope for this
pass and flagged for follow-up: application/x-www-form-urlencoded,
arrays of files, nested objects as form fields, per-part
encoding.contentType, and filenames other than the property name.lib/model_helpers.dart.
Previously every generated package shipped all nine runtime helpers
(maybeParseDateTime, maybeParseDate, maybeParseUri,
maybeParseUriTemplate, parseFromJson, listsEqual, mapsEqual,
listHash, mapHash) plus the package:collection / package:uri
imports, even when no generated code referenced them. The file
renderer now aggregates SchemaUsage/ApiUsage across every
rendered file and emits only the helpers actually called — and
skips writing model_helpers.dart entirely when the spec uses
none. Consumers running coverage on generated code see fewer
uncovered lines.fromJson rejection
contract where applicable. A new RenderSchema.invalidJsonExample
hook returns a guaranteed-invalid JSON payload — implemented for
enums (unknown string), objects with required properties (empty
map, which fails the type cast inside parseFromJson), and
date/date-time pods (unparseable string). When non-null, the
schema_round_trip_test template emits an extra
throwsFormatException test. No-op pods (string / email / uuid /
boolean / uri / uri-template) and objects with no required fields
get no negative test because any input is valid for them.toString and every enum value.
Previously each test only round-tripped exampleValue, which for
enums is values.first — the generated toString override and
every non-first variant were uncovered. Enum round-trip tests now
iterate values, asserting toString() == toJson() and
fromJson(value.toJson()) == value for each variant.Type.maybeFromJson(instance.toJson())! instead
of Type.fromJson, and assert parsed.hashCode == instance.hashCode
alongside parsed == instance. Exercises the nullable-input branch
and catches drift between the generated == and hashCode
overrides. A second test case pins Type.maybeFromJson(null) == null.$refs at generation time. A spec that refs
schemas/responses/parameters/request-bodies/headers in another file
($ref: 'shared.yaml#/components/schemas/Foo') now loads that file
transitively, parses its components: section, and registers those
objects under their absolute URIs so the resolver can find them.
The resolver tracks which document it is currently reading from and
resolves refs against that document — so refs inside an external
file (local or cross-file) resolve against the right base rather
than the root spec. External docs must be shaped as OpenAPI
components libraries ({components: {...}}); anything else surfaces
a clear FormatException. Previously, external refs hit a "Schema
not found" at resolve time because only the root spec was walked
into the registry.$ref cycles. A schema like Node whose left
and right properties both $ref back to Node previously sent
the resolver into an infinite loop trying to inline an immutable
tree. The resolver now tracks the stack of pointers currently being
resolved; a $ref back to one already on the stack emits a
ResolvedRecursiveRef cycle-break marker that renders as a
class-name reference to the original newtype. Non-cyclic refs keep
inlining as before — no behavior change for existing specs.
Generated Dart: final Node? left; final Node? right; with
recursive toJson/fromJson through the standard
Map<String, dynamic> helpers.FileRenderer emit methods as @protected override
hooks (renderPubspec, renderAnalysisOptions, renderGitignore,
renderApiException, renderAuth, renderApiClient,
renderModelHelpers, renderApis, renderClient, renderPublicApi,
renderCspellConfig) so a subclass can skip individual output files.
A generator-consumer package with a hand-maintained pubspec.yaml
(or its own HTTP client, auth, etc.) can override just those hooks
to no-op, while still regenerating models/messages/tests from the
spec. Previously these were private _render* methods, so the only
way to opt out was to overwrite the files after each regeneration.type: string, format: date-time | uri | uri-template | email | uuid | date, or type: boolean) now renders as an
extension-type newtype wrapping its Dart type — previously, named
format: date-time schemas silently inlined as raw DateTime at
every reference, losing the wrapper. Inline fields continue to use
the raw Dart type. format: email / uuid now map to a
String-backed pod; format: date maps to a DateTime with
YYYY-MM-DD JSON serialization via a new maybeParseDate helper;
format: time is accepted without warning and falls through to
SchemaString (no Dart type equivalent).test/<modelPath>_test.dart. The test builds an in-memory instance
via RenderSchema.exampleValue (implemented per subclass), then
asserts Type.fromJson(instance.toJson()) == instance. Schemas that
can't produce a safe example — recursive types, no-JSON types —
opt out automatically (propagate null up). Consumers control the
behavior with the new hooks:
GeneratorConfig.generateTests (default true): wholesale offFileRenderer.testPath(LayoutContext) → String?: return null to
skip a schema, or redirect (e.g. to test/generated/ to avoid
colliding with hand-written tests).FormatException (not raw TypeError) from generated object
fromJson factories on malformed input. A shared parseFromJson
helper in model_helpers.dart wraps the constructor call in a
try/catch that converts TypeError (which extends Error, so routes
that do on Exception catch were dropping it and returning 500) into
a FormatException that caller code can handle cleanly as
"malformed request body". Generated factories are one line:
return parseFromJson('App', json, () => App(id: json['id'] as String, ...));.a/an in generated fromJson/toJson
dartdoc (/// Converts a \Map<String, dynamic>` to an [App].instead of… a [App].). Class names starting with A/E/I/O get "an"; U is excluded since User/Uniform/Unique` start with a consonant sound.4XX/5XX range responses. Previously
only default: contributed to ApiException<T>. Now the generator
collects error schemas from default:, 4XX:, and 5XX:,
deduplicates by structural equality, and emits the typed throw when exactly one
distinct error schema remains (the common case — most specs alias
every error to a single ErrorResponse). When the error schemas
disagree across those slots, the generator falls back to untyped
ApiException<Object?> rather than lying about what callers will
catch.1XX/2XX/3XX/4XX/5XX)
in response maps. Previously the parser rejected them with "Invalid
response code". Range responses are stored separately from
specific-code responses at each pipeline layer. 2XX ranges feed the
return-type determination — an operation declaring 2XX: { schema: X }
with no explicit 200 now generates Future<X> instead of
Future<void>. Range schemas are walked for emission so the referenced
types are generated like any other response.default: response. ApiException is now generic
(ApiException<T>): when the operation has a default response schema,
the non-2xx branch throws ApiException<ErrorType>(code, raw, body: ErrorType.fromJson(...)); when it doesn't, the throw is unchanged
(ApiException(code, raw), type parameter inferred as dynamic).
Callers can catch (e) { if (e is ApiException<ErrorType>) { ... } }
or pattern-match on e.body for the parsed server error. The
existing untyped catch-alls on ApiException (no type argument)
still match.default: responses instead of silently ignoring them.
A default response is stored on Responses.defaultResponse at the
parse layer, threads through the resolver and render tree as a
separate field (the numeric-status-keyed responses are unchanged),
and is walked by the render-tree walker so its referenced schema is
always emitted — no more tree-shaking a type whose only reference is
through default:.loadAndRenderSpec now takes a
single GeneratorConfig value (replacing 8 individual named
arguments) that includes an optional fileRendererBuilder hook;
[FileRenderer] accepts a single FileRendererConfig bundle so
subclasses forward super(config) without tracking constructor-
parameter drift, and exposes a @protected
modelPath(LayoutContext) hook that returns the lib/-relative
path for a schema's file — letting subclasses redirect both the
directory and the filename. Ship a runCli entrypoint helper that
parses the standard CLI flags and runs loadAndRenderSpec so a
custom consumer entrypoint is one line. The default behavior is
unchanged; the public surface is intentionally narrow
(runCli, GeneratorConfig, FileRenderer, FileRendererConfig,
LayoutContext, RenderSchema, FileRendererBuilder) and
labelled experimental pending feedback from additional consumers.{@template <snake_name>} / {@endtemplate} block and emit
/// {@macro <snake_name>} on the generated constructor, so the
same prose documents both the class and its constructor without
duplication. Matches the handwritten Dart convention. Off when the
schema has no title/description./// Converts a Map<String, dynamic> to a [Type]. dartdoc on
generated fromJson factories and /// Converts a [Type] to a Map<String, dynamic>. on toJson methods. Object and empty-object
schemas only; newtype/enum templates keep their existing one-line
bodies uncommented (their input/output types are String/num,
not Map<String, dynamic>, so a generic doc comment would be
misleading).lib/models/<name>.dart or
lib/messages/<name>.dart (split by name suffix: classes whose name
ends in Request or Response go to messages/) instead of a flat
lib/model/ directory. Matches the conventional layout of
hand-written Dart packages (models as domain primitives, messages as
request/response DTOs). The old flat lib/model/ layout is still
produced by Quirks.openapi() via the new flatModelDir quirk, so
generators targeting OpenAPI Generator compatibility see no change.
The previous lib/model/ directory is also wiped when regenerating,
so stale files from the old layout don't linger after an upgrade.dart:convert, dart:io, package:meta/meta.dart, and
model_helpers.dart from models; gate meta and model_helpers on
body contents; filter the schema's own file from its imports. Cuts
dart fix work roughly in half on spacetraders and github.parameters. Parameters declared on a path
item now apply to every operation on that path; operation-level
parameters still override by (name, in). Previously path-item-level
parameters were silently dropped.description. Previously
the class-level description was rendered but property-level
descriptions were dropped.x-enum-descriptions vendor extension. A parallel array
of strings alongside enum: now renders as per-case dartdoc on the
generated enum.Quirks().allListsDefaultToEmpty now defaults to
false. The "nullable lists default to const []" behavior was
really an OpenAPI convention and is now only on via
Quirks.openapi(). Callers using the plain default who relied on
the old behavior should opt in explicitly:
Quirks(allListsDefaultToEmpty: true).propertyNames
keyword on map-shaped schemas. When propertyNames resolves to a
named string enum, the generated field becomes Map<EnumKey, V>
with the enum's fromJson/toJson round-tripping each key at the
boundary. Handwritten Map<ReleasePlatform, ReleaseStatus> can now
be expressed spec-compliantly without a vendor extension.api.dart barrel, narrowed with show to exactly the
names used. Model files import package:uri/uri.dart directly for
UriTemplate-typed fields, but Dart exports don't chain through
imports — a consumer (or a generated round-trip test) that imports
only the barrel couldn't reference UriTemplate without also
importing package:uri/uri.dart itself. The barrel now walks the
rendered schemas, collects every third-party package: entry from
additionalImports that has an explicit shown: list, and emits
export 'package:<pkg>/<entry>.dart' show <T1, T2, ...>; covering
only the specific types used —
export 'package:uri/uri.dart' show UriTemplate; today. Imports
without a shown: list (e.g. package:meta/meta.dart for
@immutable) stay internal to the model file. Motivating case:
the GitHub spec's Root model (~40 uri-template fields) produced
40+ undefined_function: UriTemplate errors per regeneration; now
clean.entries: field in RenderObject.exampleValue
so round-trip tests for schemas with additionalProperties compile.
When a schema has additionalProperties, the generated class carries
a required entries: Map<String, V> field alongside the named
properties — but the exampleValue generator iterated only
properties and produced a constructor call missing the required
entries argument, yielding missing_required_argument errors for
every such schema. The value type on the emitted Map literal matches
the additionalProperties.typeName — dynamic for open
additionalProperties, String for {type: string}, etc. Hit by
integration_permissions and the copilot_* family on the GitHub
spec.oneOf / anyOf. The
schema_one_of.mustache template emits a sealed class with no
concrete subclasses and an UnimplementedError-throwing fromJson,
so there's no Dart value of the sealed type that can be constructed
at compile time. Previously RenderOneOf.exampleValue returned the
first branch's own example (e.g. a raw 'example' String for
oneOf: [string, integer]), which didn't type-check against the
enclosing sealed-class field and produced errors like String can't be assigned to IssuesCreateRequestTitle — ~85 of the 156 broken
tests in the generated GitHub client. Returning null propagates
through RenderObject.exampleValue so the round-trip test is
skipped for any schema that transitively depends on a oneOf/anyOf.
Real coverage here is blocked on discriminator-aware subclass
emission (#99); today's coverage was fake.type: null properties as dynamic instead of crashing.
OpenAPI 3.1 / JSON Schema 2020-12 allows a property schema to be
{"type": "null"}, meaning "the only legal value is null"; the
parser has always produced a ResolvedNull for this, but the render
layer had no case for it and aborted with
UnimplementedError: Unknown schema: ResolvedNull at <pointer>.
Now toRenderSchema maps ResolvedNull to RenderUnknown, which
emits dynamic (same treatment as additionalProperties: true).
A dedicated RenderNull with Dart's strict Null type would be
more precise but isn't useful in practice. Found while running the
generator against a real-world spec that declares a reserved-for-
future-use "placeholder": {"type": "null"} field.RenderVoid, RenderBinary) when deciding
whether an operation has a typed error body. Previously, a spec where
the default: or 4XX/5XX response declared no content (just a
description:) would collect a RenderVoid into distinctErrorSchemas,
land in the "typed error" branch with errorType == 'void' and
errorFromJson == '', and emit uncompilable Dart like
throw ApiException<void>(code, body.toString(), body: ,); — which
failed dart format and blocked the whole generation. The fix
filters RenderNoJson out of the error-schema set so such operations
fall through to ApiException<Object?>(code, message) like any other
untyped error. Hit while running petstore, which declares only
description-only error responses.oauth2,
openIdConnect / openIDConnect, mutualTLS) at parse time instead
of crashing the whole generation. Previously, any spec that even
declared one of these schemes in components.securitySchemes died
at parser.dart:1096 with UnimplementedError, even when no
operation required the scheme. Now those declarations parse to an
UnsupportedSecurityScheme sentinel that renders as NoAuth() in
generated operations, plus a [WARN] at generation time telling the
consumer to override ApiClient.resolveAuth or set defaultHeaders
if they actually need the auth. Unblocks the standard OpenAPI
examples (petstore declares oauth2.implicit; train-travel declares
oauth2.authorizationCode) and any other real spec that advertises
oauth2/OIDC/mTLS without us having to implement them. The existing
apiKey and http flows are untouched.format: uuid, date, date-time, email, uri,
uri-template, boolean) as path parameters. Previously
_canBePathParameter only accepted ResolvedString, ResolvedInteger,
ResolvedEnum, and recursive ResolvedOneOf, so a common pattern like
/resources/{id} with id declared as type: string, format: uuid
crashed the resolver with "Path parameters must be strings or
integers". All pod types serialize to a single string via their
toJson — which is the expression interpolated into the URL path —
so they're legal path parameters. Found while running the generator
against a third-party OpenAPI spec that uses UUID path parameters
throughout.toSnakeCase so tag names with spaces (e.g.
"Payment Methods", "Seller Account") no longer survive into
generated class names as class Payment MethodsApi — a literal
space, uncompilable Dart, dart format fails, whole generation
aborts. Any whitespace run now collapses to _ before the existing
camel/kebab conversions, followed by a final _+ → _ pass to clean
up the doubled underscores the intermediate form produces.application/octet-stream and text/plain request bodies: the
generated ApiClient.invokeApi was JSON-encoding every non-null body
(including Uint8List and raw String) and always sending
Content-Type: application/json. Binary and text bodies now pass
through unchanged with the correct Content-Type header, driven by a
new BodyContentType enum threaded through the generated call sites.Api and RenderSpec from package:space_gen/space_gen.dart
so subclasses of FileRenderer can actually spell the parameter
types of the new override hooks (renderApis, renderClient,
renderPublicApi, renderApiClient). Shipped with the hooks
initially but briefly forgotten, breaking every attempt to override.with/try/case/... now
emits required String with_ (matching dartParameterName) instead
of the uncompilable required String with. RenderParameter's
template context was using raw lowercaseCamelFromSnake(name) while
every other call site went through variableSafeName, producing a
name mismatch and invalid Dart.hashCode to be consistent with == on list/map
fields. Two instances with the same list/map contents now hash to
the same value — matching the listsEqual/mapsEqual-based ==
override. Before, == returned true but hashCode differed
(identity hash on the list/map), violating the Dart contract. New
listHash/mapHash helpers in model_helpers.dart, wired through
a RenderSchema.hashCodeExpression hook.Future<void> endpoints so a successful empty body (e.g. 204
No Content) returns normally. Generated methods previously fell
through to throw ApiException.unhandled(...) whenever the body
was empty, which meant every successful DELETE/PATCH on a 204 route
threw.description: |) no longer render a dangling ///
line before the class or field they document.new, void, null, ...)
were escaped; enum values like TRY or CLASS generated
uncompilable try._('TRY') / class._('CLASS'). Now all Dart
reserved words, built-in identifiers, and contextual keywords are
escaped with a trailing underscore.RenderMap.jsonStorageType to honour isNullable. Previously a
nullable map field emitted
(json[key] as Map<String, dynamic>)?.map(...) — the cast crashed
when the key was missing/null (type 'Null' is not a subtype of type 'Map<String, dynamic>'). Now emits
(json[key] as Map<String, dynamic>?)?.map(...) so the null-aware
?.map chain actually has a nullable receiver.RenderArray.defaultValueString to return null when the
schema has no default (previously crashed casting null as List
once allListsDefaultToEmpty was off).?foo.toString(), which always produced a
map entry (with the literal string "null" as its value) because
.toString() on a null primitive returns the string "null". Now
emits ?foo?.toString(), so the null-aware map-entry operator
correctly suppresses the entry when the parameter is null.Fix finding of template files when run via dart pub run space_gen.
dart pub run space_gen.- Initial version.
Your coding agent can read these notes before it upgrades. Set up the MCP server →