One annotated class with no members:
@dmx('openApiClient')
class Frankfurter {
//#region
//#endregion
}Save the file. dmx writes a typed client, a data class per schema, and a barrel that exports the lot — nine files, none of whose names appear anywhere in hand-written source:
lib/
frankfurter.dart hand-written: the annotation, and the manifest in its region
http_transport.dart hand-written: dart:io behind the DmxTransport seam
frankfurter_client.dart generated: one method per operationId
rate.dart generated: components/schemas/Rate
currency.dart generated
currency_detail.dart generated
provider.dart generated
rate_provider.dart generated: an object the document never named
currency_detail_peg.dart generated: likewise
api.dart generated: the barrel
Nothing about the API is typed by a person. Not an endpoint, not a parameter, not a response type, not a file name.
This example shows both halves doing their own job, because it is easy to read "custom Dart macro" and "Mustache template" as a choice between two options.
The macro does what no template could. It parses the OpenAPI document,
resolves $refs into components/parameters and components/responses, reads
OpenAPI 3.1's type: ["string", "null"] as nullability, maps format: date to
DateTime in a response but to String in a query parameter, and invents names
for schemas the document wrote inline — the object inside Rate.providers[] has
no name anywhere in the document, and the macro calls it RateProvider.
The templates do what string-building should not. Every line of generated
Dart is laid out by a file in tool/dmx/templates/. Want a different
constructor, a retry, a logging hook, toJson back? Edit
templates/client.mustache and save. No Dart changes.
They meet at a plain JSON model. tool/dmx/src/context.dart turns the parsed
document into strings, booleans, and lists of maps; the templates contain no
logic beyond {{#section}}.
frankfurter.openapi.json → document.dart → context.dart → *.mustache → lib/*.dart
the source of truth parse it model it shape it emit it
dmx renders those templates itself, over the worker protocol ([dartmacros.render]) — the same Mustache, the same standalone-tag handling and the same whitespace normalizer the built-in catalogue uses. The macro never ships a template engine.
Frankfurter, an open-source exchange-rate API over European Central Bank data. It was chosen for three reasons:
- it publishes a real OpenAPI 3.1 document (vendored here as
tool/dmx/frankfurter.openapi.json, MIT-licensed, fromhttps://api.frankfurter.dev/v2/openapi.json); - the document is small but not toy — five operations, four named schemas, path
and query parameters,
$refs, enums, nullable properties, inline objects; and - its data is immutable, so tests against the live API are deterministic. The euro's reference rate against the dollar on 1999-01-04 is a fact, not a reading.
The document is vendored, not fetched. Generation must be reproducible from committed source, so a build never depends on the network.
make example-openapi # generate, analyze, and run the hermetic tests
make example-openapi-live # the same, then run the client against the real API| Suite | What it proves | Network |
|---|---|---|
test/generated_shape_test.dart |
The generated tree tracks the document — every operationId has a method, every schema has a class and a file, every file carries its ownership marker. Assertions are read out of the document at runtime, so a stale build fails. |
no |
test/decode_test.dart |
The generated decoders read payloads the live API actually returned, captured into test/fixtures/. Failure paths included: a wrong type names its property, a bad list element names its index. |
no |
test_live/live_api_test.dart |
The generated client, the generated decoders, and the real API. | yes |
The live suite lives in test_live/ rather than behind a skip, so a bare
dart test is hermetic and no CI or release gate depends on somebody else's
uptime — while the tests themselves stay real and are run by a target of their
own. Nothing here is skipped.
Deliberately narrow, and it says so rather than guessing. A generator that quietly mishandles a construct emits Dart that compiles and lies.
Read: openapi/info/servers[0].url; paths operations with an
operationId; path and query parameters, including $refs into
components/parameters; 200 responses, including $refs into
components/responses; components/schemas with type, format,
properties, required, items, enum, and OpenAPI 3.1 nullable type
arrays; and inline objects, which get a synthesized class.
Not read: request bodies (this API has none — every operation is a GET, so
nothing is generated to encode), allOf/oneOf/anyOf, additionalProperties
schemas, security schemes, non-JSON content types, and servers beyond the first.
Meeting one of these is a DmxRefusal with a code, not a silent wrong answer.
lib/ the seed, the transport, and the generated tree
tool/dmx/
macros.dart the worker: reads the document, renders, returns files
frankfurter.openapi.json the source of truth, vendored
templates/
model.mustache a data class and its decoder
client.mustache the client and its methods
barrel.mustache the export list
manifest.mustache the seed's own region
src/
document.dart OpenAPI JSON → typed model, with `$ref`s resolved
dart_types.dart a schema node → a Dart type and its decoder
context.dart the typed model → the JSON the templates render
test/ hermetic suites
test_live/ the suite that calls the API