space_gen 1.2.2
space_gen: ^1.2.2 copied to clipboard
An OpenAPI generator, focused on generating high quality code.
1.2.2 #
Bug fixes #
lines_longer_than_80_charssuppression now scopes to files space_gen actually emits and matches the analyzer's own carve-outs. The previous post-walk pass scanned every.dartfile in the output directory — including hand-written templates aFileRenderersubclass was deliberately preserving viarenderClient/renderPublicApino-op overrides. It also counted raw line length without the analyzer's URI exclusion, so files whose only over-80 lines wereimport/exportstatements got the directive added, thendart fix --applystripped it asunnecessary_ignore. Replaced with a per-file emit-time helper threaded through the same_renderTemplatepattern PR #138 settled on forcomment_references. Skipsimport/exportlines when measuring (#209).- Format generated content in-process at write time so directive
decisions match what the analyzer sees. The emit-time check from
#209 fired against pre-format content —
dart formatlater reflows long lines under 80,dart fix --applystrips the now-pointless directive asunnecessary_ignore, and the 3-line justification comment is left orphaned (61 such files surfaced on a real-world regen). Addpackage:dart_styleas a dep and runDartFormatterin-process from_renderTemplate/_renderSpecTemplatebefore applying transforms. Format settings come from the consuming package'spubspec.lock/pubspec.yaml(language version) andanalysis_options.yaml(formatter: page_width,formatter: trailing_commas), with relativeinclude:directives followed so workspace setups resolve correctly. Without per-consumer settings, hard-pinning tolatestLanguageVersionwould silently flip generated files to tall style and strip trailing commas from packages that opt intotrailing_commas: preserve. Also drops a bogus///carve-out frommaybeAddLongLineIgnore(the analyzer flags long doc comments) and extendsmaybeAddCommentReferencesIgnoreto resolve against same-file field declarations, not just top-level types — fixes the same orphan-justification problem on thecomment_referencesdirective. The runtime cost is absorbed: clean inputs makedart fix --applyslightly faster, and the globaldart formatstep becomes a near no-op for files we wrote (#210).
Refactoring #
- The directive-block helpers,
Formatter(subprocess), and the new in-processDartFileFormattermove to a dedicatedlib/src/render/formatting.dart.FileRenderernow holds aDartFileFormatterand calls into it; the formatting concerns cluster together rather than threading through the renderer (part of #210).
1.2.1 #
Chores #
- 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 against (GitHub, Discord, SpaceTraders, Train Travel, Petstore) and their current status. Add a "Powered by Shorebird" badge. Promote the OpenApi Quirks subsection to a top-level section. Still-real items from the old TODO that didn't already have tracking issues were filed as #204–#207.
1.2.0 #
Features #
oneOf/anyOfdispatch — full sealed-class generation. Previously everyoneOfbody emittedthrow UnimplementedErrorfrom the synthesized parent'sfromJson. The renderer now picks one of five strategies and emits real Dart 3 pattern-matched dispatch:- Discriminator dispatch — explicit
discriminator: {propertyName, mapping}switches onjson[propertyName](#143). - Implicit-discriminator dispatch — when each variant tags itself with a required single-value enum/const property (or per-variant pairwise-disjoint enum value sets), dispatch on it as if a discriminator were declared (#149, #182).
- Shape dispatch — variants of distinct JSON shapes
(
int/String/bool/Map/List/num) switch on the runtime type; extends throughRenderArray,RenderNumber, andRenderMapvariants (#145, #147, #151, #152). - Required-field / property-presence dispatch — object variants with
a uniquely-required (or uniquely-optional) field generate an if-chain
over
containsKey. A variant with no required fields can act as the catch-all fallback (#146, #153, #154). - Hybrid dispatch — mixed-shape
oneOfs combine an outer shape switch with required-field sub-arms insideMap<String, dynamic>, plus array-element shape sub-arms insideList<dynamic>(#156, #183). - Array-element dispatch —
anyOf<array<X>, array<Y>>peeks the first list element to pick a variant (#159). - Array-element-property shape dispatch — for object-variant twins that share required keys but differ in the items shape of an array property (validation-error / validation-error-simple) (#177).
- Discriminator dispatch — explicit
- Smoosh: variant data classes inline directly into the sealed parent.
Previously each
oneOfemitted a per-variant data class plus a wrapper subclass holdingfinal 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.dartfile — no wrapper, novalueindirection, pattern matching destructures the variant's fields directly. Covers predicate-required, discriminator, shape, hybrid, and inlineallOfvariants; also extends to top-level$refvariants used by exactly one parent (#168, #169, #170, #179, #189). Round-trip tests are synthesized for each smooshed variant (#181). Structurally-identicaloneOftrees synthesized under operation paths share one resolved schema — one Dart class instead of multiple byte-identical sealed classes (#180). - HTTP-status dispatch on operations with multiple successful responses.
Operations declaring e.g.
200: User, 202: AcceptedJob(or201: Empty, 204: ø) used to coalesce into a discriminator-lessRenderOneOfand throwUnimplementedError. Now emit a sealed parent with onefinalwrapper per status code; the api method body switches onresponse.statusCode. Empty-body and empty-schema 204s render as value-less /dynamic-valued wrappers respectively (#148). - Naming pass. A single global pass owns every Dart class name the
generator emits, including wrapper subclasses. Each entity submits a
preference list (best/shortest first, longest/safest last); the new
NameAllocatorruns an order-independent fixpoint and disambiguates remaining collisions with numeric suffixes. Title-derived names land for inlineoneOf/anyOfvariants whose spec carries a uniquetitle:. Wrappers no longer double-prefix (ProjectsCreateCardRequestProjectsCreateCardRequestOneOf0→ProjectsCreateCardRequestVariant0) (#161, #162, #163, #165, #166, #167). - Integer enums (
type: integer, enum: [...]) parse and render as typedenumclasses withvalueNvariant names (value1,valueNeg1) via a newSchemaEnum<T>family pinningTtoStringorint(#192). - OpenAPI 3.1
constparses as a single-value enum, equivalent toenum: [X]. Untypedconstinfersintegervsstringfrom the value (#198). AoneOfwhose every variant is a single-valueconstcollapses to one typed enum class — eliminates 80 stubUnimplementedErrorfactories on Discord-shaped specs (#200). - Newtype validation moves into the constructor. A newtype with
pattern/minimum/maxLengthetc. 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-callvalidatePattern(...)at API call sites is dropped for newtype params — 327 broken call sites in Discord'sdefault_api.dartcleared (#194). contentEncoding: base64on atype: stringproperty renders asUint8Listwithbase64.encode/base64.decodeapplied automatically at the JSON boundary; nullable cases route through newmaybeBase64Encode/maybeBase64Decodehelpers (#201).example:andexamples: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```jsonblocks). Schema examples use the same emission point (#178).- Inline
oneOf/anyOfat property schema slots are honored even when the property also declarestype: [null, string, integer]— the explicit collection takes precedence over the multi-type expansion, preserving per-variant detail likeformat: date-time(#174). - Object schemas with required-only
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). - Pass-through bearer auth for
oauth2andopenIdConnect. Both deliver opaque bearer tokens at the wire level; route them throughHttpSecurityScheme(scheme: 'bearer')and reuse the existing bearer plumbing. Token acquisition (grants, OIDC discovery, refresh) stays the caller's responsibility viaApiClient(readSecret: …)(#126). - Non-JSON success responses (
text/plain,text/html,application/octocat-stream, …) returnresponse.bodydirectly instead of crashing injsonDecode. JSON responses are unchanged (#127). - CLI auto-sanitizes invalid Dart package names.
space_gen -i api.github.com.json -o /tmp/api.github.comnow succeeds; the package name lands asapi_github_comand the CLI logs the substitution. Programmatic callers buildingGeneratorConfigdirectly still trip thevalidatePackageNamesafety net (#123). space_gendeclares itsexecutables:sodart pub global activate space_genputs aspace_gencommand on PATH (#118).example/directory with a minimal petstore spec and CLI walkthrough, for pub.dev's package-scoringexample/check (#117).- Auto-publish to pub.dev on tag push via the canonical
dart-lang/setup-dartreusable workflow with OIDC auth (#116).
Bug fixes #
- Round-trip correctness for nullable
oneOf,EmptyObject, andadditionalProperties. A nullableoneOfproperty emittedas Map<String, dynamic>(non-nullable) and crashed on null round-trip;RenderEmptyObjecthard-codedconst $className()on read andconst <String, dynamic>{}on write, dropping inner state; theadditionalPropertiesfor-loop swept named-property keys into the catch-allentriesfield. Closes 41 pre-existing failing round-trip tests on the github regen (#184). - Required-and-nullable keys (OpenAPI 3.1
type: [T, "null"]plus the property inrequired) now route through a newcheckedKeyhelper that verifiescontainsKeybefore reading. Previously the generatedfromJsonsilently accepted a missing key as null, contradicting the generated round-trip test (#121). allOfsynthesis merges required properties. TheResolvedAllOf → RenderObjectsynthesis dropped each member'srequiredPropertieslists, silently making required fields nullable in the constructor and hiding required-property tags fromoneOfdispatch detection (#150).- Don't drop properties / required when
oneOfis a sibling. Atype: objectschema with bothproperties/requiredANDoneOf/anyOfsiblings 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). - Multi-status sealed wrapper name collision. When a top-level
schema and a multi-status operation's synthesized response wrapper
shared a name (e.g.
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). - Array query / header parameters serialize per OpenAPI defaults:
query (
style=form, explode=true) emits?key=v1&key=v2; headers (style=simple, explode=false) comma-join into one value. Previously every array param shippedList.toString()URL-encoded as?tags=%5Ba%2C+b%5D(#134). Non-defaultstyle/explode/allowReservednow warn instead of silently producing wrong output (#141). - Trailing
?on URLs without query params is dropped (Uri.replacealways appends?even when the merged map is empty) (#140). - Schema defaults are substituted in
fromJsonfor nullable const-default fields. PreviouslyFoo.fromJson({}).isFavorited == nulleven though the spec declareddefault: false. Also fixes a latentWaitTimer(null)crash inRenderNumber.defaultValueString(#128). - Bool newtypes suppress
avoid_positional_boolean_parametersvia a per-file// ignore_for_file:directive — the type name is the disambiguation. Directive carries its own justification block to satisfydocument_ignores(#124, #129). comment_referencesin 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).- Backtick code spans stay on one line when wrapping doc comments —
wrapLinesnow tracks backtick parity per line and refuses to break inside an open span. Closesunintended_html_in_doc_commenthits caused by long backticked descriptions like`heads/<branch name>`(#125). - Dotted schema names (
.App.Features.Marketplace.Order.Foofrom .NET / NSwag-style specs) collapse to underscores intoSnakeCase, not pass through verbatim — fixes a hard-fail on real specs (#119). - Non-enum identifiers always camelCase under
quirks.screamingCapsEnums. The quirk's gate previously also covered property and parameter names, so snake_case spec-side identifiers like petstore'sapi_keysurvived into Dart variables and trippednon_constant_identifier_names. Enum SCREAMING_CAPS preservation is unaffected (#122). - Required-param validators cascade. A required (non-nullable)
parameter with multiple validators emits a
..cascade chain (id..validateMaximum(10)..validateMinimum(1)) instead of duplicated receiver statements, silencingcascade_invocations(#197). model_helpers.dartis no longer a single source of "noisy" warnings:unknown formatwarnings demote to detail logs (#131); a cluster of spurious "unused" warnings on misplaced spec fields (vendorx-*,maxProperties, misplacedrequired,webhooks,externalDocs) is silenced (#176).
Refactoring #
- The
oneOfdispatch picker lifts off of the render tree and into a dedicateddispatch.dartphase that operates on the resolved tree. Dispatch decisions are a sealedDispatchDecisionfamily (DiscriminatorDispatch,ShapeDispatch,HybridDispatch,PredicateDispatch,NoDispatch); the if-chain tag-field is a sealedPredicateIR (KeyExists,ArrayElementHasKey,PropertyArrayFirstIsType,PropertyArrayItemShape,Always); the per-mode template-context shape is a sealed_DispatchModeso the type system enforces "exactly one mode active" (#157, #158, #160).
1.1.0 #
Features #
- Honor
security: []on an operation as an explicit override of the global security requirement (public endpoint), rather than silently inheriting from the spec-levelsecurity. 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 anauthRequest:argument pointing at the global scheme. Covered by a newgen_tests/security.jsonfixture that exercises the Shorebird-shaped case: global bearer auth, one endpoint opting out viasecurity: [], and one endpoint swapping to anapiKeyscheme. - Support
multipart/form-datarequest bodies. Endpoints whose request body schema is an object of scalar +format: binaryproperties now generate a Dart method that takes the body object, unpacks it inline into text fields andhttp.MultipartFile.fromBytesfile parts, and sends via a newApiClient.invokeApiMultipartruntime method (which builds anhttp.MultipartRequestwith its own Content-Type boundary). Objects with binary properties correctly throwUnsupportedErrorfromtoJson/fromJsoninstead 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-partencoding.contentType, and filenames other than the property name. - Prune unused helpers from the generated
lib/model_helpers.dart. Previously every generated package shipped all nine runtime helpers (maybeParseDateTime,maybeParseDate,maybeParseUri,maybeParseUriTemplate,parseFromJson,listsEqual,mapsEqual,listHash,mapHash) plus thepackage:collection/package:uriimports, even when no generated code referenced them. The file renderer now aggregatesSchemaUsage/ApiUsageacross every rendered file and emits only the helpers actually called — and skips writingmodel_helpers.dartentirely when the spec uses none. Consumers running coverage on generated code see fewer uncovered lines. - Generated round-trip tests now assert the
fromJsonrejection contract where applicable. A newRenderSchema.invalidJsonExamplehook returns a guaranteed-invalid JSON payload — implemented for enums (unknown string), objects with required properties (empty map, which fails the type cast insideparseFromJson), anddate/date-timepods (unparseable string). When non-null, theschema_round_trip_testtemplate emits an extrathrowsFormatExceptiontest. 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. - Round-trip tests now also cover
toStringand every enum value. Previously each test only round-trippedexampleValue, which for enums isvalues.first— the generatedtoStringoverride and every non-first variant were uncovered. Enum round-trip tests now iteratevalues, assertingtoString() == toJson()andfromJson(value.toJson()) == valuefor each variant. - Round-trip through
Type.maybeFromJson(instance.toJson())!instead ofType.fromJson, and assertparsed.hashCode == instance.hashCodealongsideparsed == instance. Exercises the nullable-input branch and catches drift between the generated==andhashCodeoverrides. A second test case pinsType.maybeFromJson(null) == null. - Resolve external
$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 itscomponents: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 clearFormatException. Previously, external refs hit a "Schema not found" at resolve time because only the root spec was walked into the registry. - Support recursive
$refcycles. A schema likeNodewhoseleftandrightproperties both$refback toNodepreviously 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$refback to one already on the stack emits aResolvedRecursiveRefcycle-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 recursivetoJson/fromJsonthrough the standardMap<String, dynamic>helpers. - Detect name collisions in the spec and rename colliding schemas before emission, rather than writing one Dart file on top of another and silently losing types. When two schemas land with the same flattened class name, the resolver appends a disambiguating suffix drawn from the collision's JSON-pointer context so the generated files don't overwrite each other. Non-rendered types (schemas that never reach a separate file) are excluded from the collision set, so an inline placeholder with a short name doesn't force a rename on a real newtype.
- Expose the
FileRendereremit methods as@protectedoverride 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-maintainedpubspec.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. - Honor the "named → newtype" invariant for pod schemas, and wire up
more string formats. A top-level named schema whose body is a string
or boolean pod (
type: string, format: date-time | uri | uri-template | email | uuid | date, ortype: boolean) now renders as an extension-type newtype wrapping its Dart type — previously, namedformat: date-timeschemas silently inlined as rawDateTimeat every reference, losing the wrapper. Inline fields continue to use the raw Dart type.format: email/uuidnow map to aString-backed pod;format: datemaps to aDateTimewithYYYY-MM-DDJSON serialization via a newmaybeParseDatehelper;format: timeis accepted without warning and falls through toSchemaString(no Dart type equivalent). - Emit a round-trip test for each generated newtype by default at
test/<modelPath>_test.dart. The test builds an in-memory instance viaRenderSchema.exampleValue(implemented per subclass), then assertsType.fromJson(instance.toJson()) == instance. Schemas that can't produce a safe example — recursive types, no-JSON types — opt out automatically (propagatenullup). Consumers control the behavior with the new hooks:GeneratorConfig.generateTests(defaulttrue): wholesale offFileRenderer.testPath(LayoutContext) → String?: returnnullto skip a schema, or redirect (e.g. totest/generated/to avoid colliding with hand-written tests).
- Throw
FormatException(not rawTypeError) from generated objectfromJsonfactories on malformed input. A sharedparseFromJsonhelper inmodel_helpers.dartwraps the constructor call in a try/catch that convertsTypeError(which extendsError, so routes that doon Exception catchwere dropping it and returning 500) into aFormatExceptionthat caller code can handle cleanly as "malformed request body". Generated factories are one line:return parseFromJson('App', json, () => App(id: json['id'] as String, ...));. - Pick grammatically correct
a/anin generatedfromJson/toJsondartdoc (/// 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 sinceUser/Uniform/Unique` start with a consonant sound. - Extend typed error bodies to
4XX/5XXrange responses. Previously onlydefault:contributed toApiException<T>. Now the generator collects error schemas fromdefault:,4XX:, and5XX:, 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 singleErrorResponse). When the error schemas disagree across those slots, the generator falls back to untypedApiException<Object?>rather than lying about what callers will catch. - Accept OpenAPI range status code keys (
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.2XXranges feed the return-type determination — an operation declaring2XX: { schema: X }with no explicit 200 now generatesFuture<X>instead ofFuture<void>. Range schemas are walked for emission so the referenced types are generated like any other response. - Generate a typed error body on generated API methods whose operation
declares a
default:response.ApiExceptionis now generic (ApiException<T>): when the operation has a default response schema, the non-2xx branch throwsApiException<ErrorType>(code, raw, body: ErrorType.fromJson(...)); when it doesn't, the throw is unchanged (ApiException(code, raw), type parameter inferred asdynamic). Callers cancatch (e) { if (e is ApiException<ErrorType>) { ... } }or pattern-match one.bodyfor the parsed server error. The existing untyped catch-alls onApiException(no type argument) still match. - Parse OpenAPI
default:responses instead of silently ignoring them. A default response is stored onResponses.defaultResponseat 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 throughdefault:. - Experimental: expose a small customization surface for callers
with their own layout conventions.
loadAndRenderSpecnow takes a singleGeneratorConfigvalue (replacing 8 individual named arguments) that includes an optionalfileRendererBuilderhook; [FileRenderer] accepts a singleFileRendererConfigbundle so subclasses forwardsuper(config)without tracking constructor- parameter drift, and exposes a@protectedmodelPath(LayoutContext)hook that returns thelib/-relative path for a schema's file — letting subclasses redirect both the directory and the filename. Ship arunClientrypoint helper that parses the standard CLI flags and runsloadAndRenderSpecso 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. - Wrap a schema's class-level description in a dartdoc
{@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. - Emit
/// Converts a Map<String, dynamic> to a [Type].dartdoc on generatedfromJsonfactories and/// Converts a [Type] to a Map<String, dynamic>.ontoJsonmethods. Object and empty-object schemas only; newtype/enum templates keep their existing one-line bodies uncommented (their input/output types areString/num, notMap<String, dynamic>, so a generic doc comment would be misleading). - Breaking: Schemas are now emitted to
lib/models/<name>.dartorlib/messages/<name>.dart(split by name suffix: classes whose name ends inRequestorResponsego tomessages/) instead of a flatlib/model/directory. Matches the conventional layout of hand-written Dart packages (models as domain primitives, messages as request/response DTOs). The old flatlib/model/layout is still produced byQuirks.openapi()via the newflatModelDirquirk, so generators targeting OpenAPI Generator compatibility see no change. The previouslib/model/directory is also wiped when regenerating, so stale files from the old layout don't linger after an upgrade. - Emit only the imports a rendered model/api body actually needs: drop
unconditional
dart:convert,dart:io,package:meta/meta.dart, andmodel_helpers.dartfrom models; gatemetaandmodel_helperson body contents; filter the schema's own file from its imports. Cutsdart fixwork roughly in half on spacetraders and github. - Support path-item-level
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. - Emit per-field dartdoc from schema property
description. Previously the class-level description was rendered but property-level descriptions were dropped. - Support the
x-enum-descriptionsvendor extension. A parallel array of strings alongsideenum:now renders as per-case dartdoc on the generated enum. - Breaking:
Quirks().allListsDefaultToEmptynow defaults tofalse. The "nullable lists default toconst []" behavior was really an OpenAPI convention and is now only on viaQuirks.openapi(). Callers using the plain default who relied on the old behavior should opt in explicitly:Quirks(allListsDefaultToEmpty: true). - Honour the JSON Schema 2020-12 / OpenAPI 3.1
propertyNameskeyword on map-shaped schemas. WhenpropertyNamesresolves to a named string enum, the generated field becomesMap<EnumKey, V>with the enum'sfromJson/toJsonround-tripping each key at the boundary. HandwrittenMap<ReleasePlatform, ReleaseStatus>can now be expressed spec-compliantly without a vendor extension.
Bug fixes #
- Re-export third-party types referenced in public field signatures
from the
api.dartbarrel, narrowed withshowto exactly the names used. Model files importpackage:uri/uri.dartdirectly forUriTemplate-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 referenceUriTemplatewithout also importingpackage:uri/uri.dartitself. The barrel now walks the rendered schemas, collects every third-partypackage:entry fromadditionalImportsthat has an explicitshown:list, and emitsexport 'package:<pkg>/<entry>.dart' show <T1, T2, ...>;covering only the specific types used —export 'package:uri/uri.dart' show UriTemplate;today. Imports without ashown:list (e.g.package:meta/meta.dartfor@immutable) stay internal to the model file. Motivating case: the GitHub spec'sRootmodel (~40 uri-template fields) produced 40+undefined_function: UriTemplateerrors per regeneration; now clean. - Include the synthetic
entries:field inRenderObject.exampleValueso round-trip tests for schemas withadditionalPropertiescompile. When a schema hasadditionalProperties, the generated class carries a requiredentries: Map<String, V>field alongside the named properties — but the exampleValue generator iterated onlypropertiesand produced a constructor call missing the requiredentriesargument, yieldingmissing_required_argumenterrors for every such schema. The value type on the emitted Map literal matches theadditionalProperties.typeName—dynamicfor open additionalProperties,Stringfor{type: string}, etc. Hit byintegration_permissionsand thecopilot_*family on the GitHub spec. - Skip round-trip test generation for schemas whose type is (or
transitively contains a required)
oneOf/anyOf. Theschema_one_of.mustachetemplate emits a sealed class with no concrete subclasses and anUnimplementedError-throwingfromJson, so there's no Dart value of the sealed type that can be constructed at compile time. PreviouslyRenderOneOf.exampleValuereturned the first branch's own example (e.g. a raw'example'String foroneOf: [string, integer]), which didn't type-check against the enclosing sealed-class field and produced errors likeString can't be assigned to IssuesCreateRequestTitle— ~85 of the 156 broken tests in the generated GitHub client. Returningnullpropagates throughRenderObject.exampleValueso 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. - Render
type: nullproperties asdynamicinstead of crashing. OpenAPI 3.1 / JSON Schema 2020-12 allows a property schema to be{"type": "null"}, meaning "the only legal value isnull"; the parser has always produced aResolvedNullfor this, but the render layer had no case for it and aborted withUnimplementedError: Unknown schema: ResolvedNull at <pointer>. NowtoRenderSchemamapsResolvedNulltoRenderUnknown, which emitsdynamic(same treatment asadditionalProperties: true). A dedicatedRenderNullwith Dart's strictNulltype 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. - Skip no-JSON schemas (
RenderVoid,RenderBinary) when deciding whether an operation has a typed error body. Previously, a spec where thedefault:or 4XX/5XX response declared no content (just adescription:) would collect aRenderVoidintodistinctErrorSchemas, land in the "typed error" branch witherrorType == 'void'anderrorFromJson == '', and emit uncompilable Dart likethrow ApiException<void>(code, body.toString(), body: ,);— which faileddart formatand blocked the whole generation. The fix filtersRenderNoJsonout of the error-schema set so such operations fall through toApiException<Object?>(code, message)like any other untyped error. Hit while running petstore, which declares only description-only error responses. - Accept unsupported security scheme declarations (
oauth2,openIdConnect/openIDConnect,mutualTLS) at parse time instead of crashing the whole generation. Previously, any spec that even declared one of these schemes incomponents.securitySchemesdied atparser.dart:1096withUnimplementedError, even when no operation required the scheme. Now those declarations parse to anUnsupportedSecuritySchemesentinel that renders asNoAuth()in generated operations, plus a[WARN]at generation time telling the consumer to overrideApiClient.resolveAuthor setdefaultHeadersif they actually need the auth. Unblocks the standard OpenAPI examples (petstore declaresoauth2.implicit; train-travel declaresoauth2.authorizationCode) and any other real spec that advertises oauth2/OIDC/mTLS without us having to implement them. The existingapiKeyandhttpflows are untouched. - Allow pod schemas (
format: uuid,date,date-time,email,uri,uri-template,boolean) as path parameters. Previously_canBePathParameteronly acceptedResolvedString,ResolvedInteger,ResolvedEnum, and recursiveResolvedOneOf, so a common pattern like/resources/{id}withiddeclared astype: string, format: uuidcrashed the resolver with "Path parameters must be strings or integers". All pod types serialize to a single string via theirtoJson— 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. - Normalize whitespace in
toSnakeCaseso tag names with spaces (e.g."Payment Methods","Seller Account") no longer survive into generated class names asclass Payment MethodsApi— a literal space, uncompilable Dart,dart formatfails, 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. - Fix
application/octet-streamandtext/plainrequest bodies: the generatedApiClient.invokeApiwas JSON-encoding every non-null body (includingUint8Listand rawString) and always sendingContent-Type: application/json. Binary and text bodies now pass through unchanged with the correctContent-Typeheader, driven by a newBodyContentTypeenum threaded through the generated call sites. - Export
ApiandRenderSpecfrompackage:space_gen/space_gen.dartso subclasses ofFileRenderercan 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. - Escape reserved words in generated API method parameter names. A
spec with a parameter literally named
with/try/case/... now emitsrequired String with_(matchingdartParameterName) instead of the uncompilablerequired String with.RenderParameter's template context was using rawlowercaseCamelFromSnake(name)while every other call site went throughvariableSafeName, producing a name mismatch and invalid Dart. - Fix generated
hashCodeto be consistent with==on list/map fields. Two instances with the same list/map contents now hash to the same value — matching thelistsEqual/mapsEqual-based==override. Before,==returned true buthashCodediffered (identity hash on the list/map), violating the Dart contract. NewlistHash/mapHashhelpers inmodel_helpers.dart, wired through aRenderSchema.hashCodeExpressionhook. - Fix
Future<void>endpoints so a successful empty body (e.g. 204 No Content) returns normally. Generated methods previously fell through tothrow ApiException.unhandled(...)whenever the body was empty, which meant every successful DELETE/PATCH on a 204 route threw. - Trim trailing whitespace off doc-comment text so YAML block-scalar
descriptions (
description: |) no longer render a dangling///line before the class or field they document. - Complete the Dart reserved-word list used to escape identifiers.
Previously only a handful of keywords (
new,void,null, ...) were escaped; enum values likeTRYorCLASSgenerated uncompilabletry._('TRY')/class._('CLASS'). Now all Dart reserved words, built-in identifiers, and contextual keywords are escaped with a trailing underscore. - Fix
RenderMap.jsonStorageTypeto honourisNullable. 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?.mapchain actually has a nullable receiver. - Fix
RenderArray.defaultValueStringto returnnullwhen the schema has no default (previously crashed castingnull as ListonceallListsDefaultToEmptywas off). - Fix nullable primitive query parameters to be null-safe. Generated
code previously emitted
?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.
1.0.1 #
- Fix finding of template files when run via
dart pub run space_gen.
1.0.0 #
- Initial version.