flap
Fast, zero-dependency OpenAPI → Dart/Flutter client generator.
flap lowers OpenAPI 3.0, 3.1, and Swagger 2.0 specs into idiomatic, production-ready Dart/Flutter clients using package:freezed and json_serializable — no Java, no Node.js, no Docker required.
Table of contents
- Features
- Installation
- Quick start
- Output layout
- Generated client
- PATCH / tri-state fields
- Schema mapping
- Programmatic use
- Project structure
- CLI reference
- Dependencies
- CI / release workflow
- License
- Author
Features
- Spec support — OpenAPI 3.0, OpenAPI 3.1, and Swagger 2.0 (YAML or JSON, local file or remote URL)
- Two HTTP backends —
package:dio(default, full-featured) orpackage:http(lightweight) - Sound null safety — Dart 3 /
freezed3 output; legacy null-unsafe output is available opt-in via--null-unsafe - Full schema coverage — objects, arrays, maps, enums, discriminated unions (
oneOf+ discriminator), untagged unions (anyOf/oneOf),allOfinheritance, recursive types - PATCH tri-state semantics —
Optional<T?>wrapper fornullable: true+ optional fields, distinguishing "key absent" from "key explicitly null" - Security schemes —
apiKey(header, query, cookie), HTTP Bearer, HTTP Basic, OAuth 2.0, OpenID Connect; credentials injected via a Dio interceptor or per-requesthttpheaders/query/cookies automatically - Response headers — typed Dart 3 named-record return types for operations with declared response headers
default:values — emitted as Freezed@Default(...)annotationsdate-timeformat →DateTime;float/doubleformat →double;binaryformat →List<int>- Parameter serialization — OpenAPI
style/explode(form,deepObject,spaceDelimited,pipeDelimited) and SwaggercollectionFormatfor query parameters; form-encoded bodies use bracket keys for nested objects - Battle-tested — the full Stripe OpenAPI document (594 operations, 1,454 schemas) generates, builds with
build_runner, and analyzes cleanly with both backends - Lenient loading — inline objects become named classes, root-level enums/primitives are supported,
$refparameters/responses/request bodies and path-level parameters resolve, and unsupported constructs degrade todynamicwith a warning instead of aborting - Multipart uploads —
multipart/form-datarequest bodies viaFormData/MultipartRequest - Multiple servers —
servers:array emitted as a typedabstract final class FooClientUrls - Incremental builds — lockfile per spec × backend keyed on the spec's SHA-256; unchanged specs are skipped
- Template overrides — swap any generated file with a Jinja2 template or a verbatim file
- Type/import mapping — replace generated schema types with your own hand-written classes
Installation
dart pub global activate flap
On first run, flap downloads the pre-built Rust binary for your platform and caches it at ~/.flap/bin/<version>/. Subsequent runs are instant.
Supported platforms
| Platform | Architecture | Asset |
|---|---|---|
| Linux | x86_64 | flap-linux-x64.tar.gz |
| Linux | arm64 | flap-linux-arm64.tar.gz |
| macOS | x86_64 | flap-macos-x64.tar.gz |
| macOS | arm64 (Apple Silicon) | flap-macos-arm64.tar.gz |
| Windows | x86_64 | flap-windows-x64.zip |
Build from source
Requires the Rust toolchain (stable).
git clone https://github.com/miracle101000/flap-dart
cd flap-dart
cargo build --release --bin generate_dart
Point the Dart wrapper at the result with the FLAP_BINARY environment variable:
FLAP_BINARY=$PWD/target/release/generate_dart flap --out ./generated path/to/spec.yaml
Quick start
# Generate a Dio client (default)
flap --out ./generated path/to/openapi.yaml
# Generate an http-package client
flap --out ./generated --client=http path/to/openapi.yaml
# Generate from a remote URL
flap --out ./generated https://petstore3.swagger.io/api/v3/openapi.yaml
# Multiple specs in one pass
flap --out ./generated specs/users.yaml specs/payments.yaml
# Force regeneration even if the spec has not changed
flap --out ./generated --force path/to/openapi.yaml
Each spec gets its own subdirectory under --out, named after the spec file stem (e.g. openapi.yaml → generated/openapi/). Inside it you will find one Dart file per schema (plus one per inline enum), one client file, flap_utils.dart (the Optional<T?> runtime), and a .flap.lock.* incremental build marker. Pass --null-unsafe to additionally emit legacy null-unsafe code into a null_unsafe/ subfolder — note that Dart 3 SDKs cannot compile that dialect.
Output layout
generated/
└── petstore/
├── pet.dart # @freezed model
├── pets.dart # typedef List<Pet>
├── error_model.dart # @freezed model (`Error` is renamed to avoid dart:core)
├── swagger_petstore_client.dart # API client
├── flap_utils.dart # Optional<T?> runtime + converters
└── .flap.lock.null_safe.dio
Generated client
Dio (default)
final client = SwaggerPetstoreClient(
baseUrl: SwaggerPetstoreClientUrls.server0, // when multiple servers declared
bearerAuth: myJwtToken, // or apiKey: ..., appId: ...
);
// Typed return — body deserialized automatically
final pet = await client.showPetById(petId: '42');
// Response headers included in a named record when declared in the spec
final (:body, :xNext) = await client.listPets(limit: 10);
// Enum parameters send their wire value; DateTime parameters are ISO 8601;
// path parameters are URI-encoded.
http
final client = SwaggerPetstoreClient(
bearerAuth: myJwtToken,
client: myCustomHttpClient, // optional; defaults to http.Client()
);
try {
final pet = await client.showPetById(petId: '42');
} on SwaggerPetstoreClientException catch (e) {
print('${e.statusCode}: ${e.body}'); // thrown for every non-2xx response
}
PATCH / tri-state fields
For nullable: true + optional fields (the "PATCH cell" — absent vs. explicitly null), flap emits Optional<T?>:
await client.updateUser(
id: '42',
body: UpdateUserRequest(
id: 'required-field',
bio: null, // required + nullable → String?
nickname: Optional.present(null), // optional + nullable → set to null
displayName: Optional.absent(), // optional + nullable → omit key entirely
),
);
Classes with such fields opt out of the freezed-generated toJson and emit one that omits Optional.absent() keys. flap_utils.dart provides Optional, Optional.of(value) (absent when value is null) and the per-type JsonConverters (OptionalStringConverter, OptionalIntConverter, …) the models use. Wire-side null on input deserialises as Optional.absent().
Enums are emitted as enhanced enums with a value field and an unknown fallback, so a server that adds a new value never breaks deserialisation:
final kind = ShapeKind.fromJson('circle'); // ShapeKind.circle
final later = ShapeKind.fromJson('hexagon'); // ShapeKind.unknown
print(kind.value); // 'circle'
Schema mapping
Type mapping
Replace a generated schema with your own class:
flap --out ./generated \
--type-map=Pet=MyAppPet \
--import-map=MyAppPet=package:myapp/models/pet.dart \
path/to/petstore.yaml
The generated client will import and use MyAppPet wherever Pet would have appeared. The Pet model file is not emitted.
Template overrides
Customise any generated file via Jinja2 or verbatim replacement:
flap --out ./generated --template-dir=./templates path/to/openapi.yaml
Resolution order per output file:
{template-dir}/{exact-filename}— verbatim copy, highest priority{template-dir}/model.dart.jinja— Jinja2 template applied to every model{template-dir}/client.dart.jinja— Jinja2 template applied to the client{template-dir}/flap_utils.dart— verbatim override for the runtime file- Built-in emitter — fallback
The Jinja2 context exposes class_name, schema_name, fields (with dart_name, dart_type, required, nullable, uses_optional_wrapper, json_name, default_expr), imports, extends, null_safety, and more.
Programmatic use
import 'package:flap/flap.dart';
Future<void> main() async {
final exitCode = await FlapRunner().run([
'--out', './generated',
'--client=http',
'api/openapi.yaml',
]);
if (exitCode != 0) throw Exception('flap failed with exit code $exitCode');
}
Project structure
flap/ # Dart package (pub.flutter-io.cn distribution)
├── bin/flap.dart # CLI entry point
├── lib/
│ ├── flap.dart # Public API export
│ └── src/
│ ├── runner.dart # FlapRunner — locates and invokes the binary
│ ├── binary_manager.dart # Downloads and caches the platform binary
│ ├── platform_info.dart # Platform slug / archive extension helpers
│ └── version.dart # Package version (must match pubspec.yaml)
├── test/ # Dart wrapper tests
│
crates/ # Rust workspace
├── flap-ir/ # Intermediate representation (language-agnostic)
│ └── src/lib.rs # Api, Schema, Operation, TypeRef, SecurityScheme …
├── flap-spec/ # OpenAPI / Swagger loader and lowering pass
│ └── src/
│ ├── lib.rs # OpenAPI 3.x parser, validation and lowering → IR
│ └── swagger.rs # Swagger 2.0 raw types, translated into the 3.x shape
└── flap-emit-dart/ # Dart code emitter
└── src/lib.rs # emit_models(), emit_client(), Jinja2 support
│
src/
└── bin/generate_dart.rs # Rust CLI binary (the thing Dart shells out to)
│
tests/generate.rs # Fixture-driven integration tests for lowering, emission and the CLI
fixtures/ # OpenAPI / Swagger spec fixtures used in tests
Crate responsibilities
| Crate | Role |
|---|---|
flap-ir |
Pure data types. No YAML, no Dart. The contract between loader and emitter. |
flap-spec |
Parses raw YAML/JSON (OpenAPI 3.x, or Swagger 2.0 translated into the 3.x shape), validates $ref integrity, operationId uniqueness and security scheme references, then lowers to IR — degrading unsupported constructs to dynamic with warnings. |
flap-emit-dart |
Consumes IR; emits @freezed models, client files, and flap_utils.dart. Supports Jinja2 template overrides via minijinja. |
CLI reference
flap --out <dir> [options] <spec> [<spec> ...]
Arguments:
<spec> Path to a local .yaml/.yml/.json file, or an http(s):// URL.
OpenAPI 3.x and Swagger 2.0 are detected automatically.
Multiple specs may be provided; each is written to its own subdirectory.
Options:
--out, -o <dir> Output directory (required).
--client=<backend> HTTP client backend: dio (default) or http.
--force, -f Regenerate even if the spec has not changed.
--null-unsafe Additionally emit legacy null-unsafe code to <dir>/<spec>/null_unsafe/.
--type-map=A=B Replace schema A with Dart type B in all generated files.
May be repeated.
--import-map=B=pkg:... Import URI for a type introduced by --type-map.
May be repeated.
--template-dir, -t <d> Directory of Jinja2 / verbatim template overrides.
--help, -h Print usage.
--version, -V Print the version.
Non-fatal spec problems (unsupported constructs degraded to dynamic, skipped headers, …) are printed as warnings; dangling $refs and duplicate operationIds fail the run.
Set FLAP_BINARY=/path/to/generate_dart to make the Dart wrapper use a locally built generator instead of downloading a release binary.
Dependencies
The generated Dart code requires these packages in the consuming project:
# pubspec.yaml
environment:
sdk: ^3.8.0 # json_serializable ≥ 6.9 emits null-aware elements
dependencies:
dio: ^5.0.0 # if using --client=dio (default)
http: ^1.0.0 # if using --client=http
freezed_annotation: ^3.0.0
json_annotation: ^4.12.0
dev_dependencies:
build_runner: ^2.4.0
freezed: ^3.0.0
json_serializable: ^6.9.0
After generation, run the build runner once:
dart run build_runner build --delete-conflicting-outputs
CI / release workflow
The GitHub Actions workflow (.github/workflows/ci.yml) runs on every push and pull request:
- Rust —
cargo fmt,cargo clippy,cargo test(unit + fixture-driven integration tests) on Ubuntu, macOS, and Windows - Dart —
dart analyze,dart test,dart pub publish --dry-run
The release workflow (.github/workflows/release.yml) triggers on v* tags:
- Checks the tag matches
pubspec.yaml, then builds the Rust binary for all five targets in parallel - Packages each into a
.tar.gz(Unix) or.zip(Windows) with a SHA-256 sidecar - Creates a GitHub Release and attaches all archives
- Publishes the Dart package to pub.flutter-io.cn via OIDC
License
MIT — see LICENSE.
Author
Libraries
- flap
- flap — OpenAPI → Dart/Flutter client generator.