dart_openapi_generator 0.3.0
dart_openapi_generator: ^0.3.0 copied to clipboard
Native-Dart, OpenAPI 3.x → Dart code generator. Generates idiomatic Dio-based model classes and API client services via build_runner. No JVM. No Mustache. Generated code looks hand-written.
dart_openapi_generator #
A native-Dart build_runner builder that reads a local OpenAPI 3.x spec and generates Dio-based model classes and API service classes. No Mustache templates. Generated code looks like hand-written Dart.
Add as a dev dependency only — it is never in your app's runtime dependency tree.
Documentation #
Full documentation at docs.page/angeloavv/dart_openapi_generator.
How it works #
build_runnerinvokes thedartOpenApiBuilderfactory once, synchronously — before it freezes its asset graph for the invocation.- The factory reads
input_spec/output_dirand other options from yourbuild.yaml, loads the spec from disk (local files only — no HTTPS), and runs the full pipeline: parse into an internal document model (schemas, operations, security schemes), thenModelGenerator,ServiceGenerator,AggregatorGeneratoremit Dart source, formatted withdart_style. - The resulting
Builderinstance'sbuildExtensionsdeclares the real, per-project list of output files computed in step 2 — so build_runner tracks every generated file in its asset graph from the start. build()itself just writes the precomputed files viabuildStep.writeAsString— since they're declared outputs, downstream builders (e.g. your ownsource_gen-based builder) resolve them correctly in the samebuild_runner buildinvocation, no second pass needed.
Caching and cleanup of stale outputs are handled by build_runner's own incremental engine and --delete-conflicting-outputs — nothing to configure. See HANDOVER-single-pass-build.md for the full incident writeup behind this design.
Installation #
# pubspec.yaml
dependencies:
dio: ^5.0.0
dev_dependencies:
dart_openapi_generator: ^0.2.0
build_runner: ^2.4.0
Setup #
Configure the builder in your project's build.yaml:
# build.yaml
targets:
$default:
builders:
dart_openapi_generator:
options:
input_spec: "openapi/my_api.yaml"
output_dir: "lib/generated"
client_name: "MyApiClient"
Run generation:
dart run build_runner build
Or watch for changes:
dart run build_runner watch
Configuration reference #
All parameters are set in the options: block of your build.yaml (see Setup).
| Option | Type | Default | Description |
|---|---|---|---|
input_spec |
String |
required | Local spec file path, relative to package root. No remote/HTTPS spec URLs. |
output_dir |
String |
required | Directory for generated files, relative to package root. The barrel file is named after this path's last segment — see Generated output structure. |
client_name |
String |
'ApiClient' |
Name of the generated aggregator class |
date_time_converter |
String |
'iso8601' |
'iso8601' → ISO 8601 strings; 'timestamp' → milliseconds since epoch |
debug_logging |
bool |
false |
Log every file prepared |
Why build.yaml and not a Dart annotation: input_spec/output_dir must be readable synchronously, in the builder factory, before build_runner freezes its asset graph — annotation resolution requires a build already in progress, so it can't run early enough. See How it works.
Generated output structure #
Given output_dir: 'lib/generated', the builder writes:
lib/generated/
generated.dart # barrel — exports everything below, named after output_dir's last segment
api_client.dart # aggregator class with one field per tag + auth factories
models/
user.dart # one file per OpenAPI component schema
user_role.dart
...
services/
users_api.dart # one file per tag (or 'default_api.dart' for untagged ops)
auth_api.dart
...
The barrel file's name is derived from output_dir's last path segment, not hardcoded — this keeps multiple generated clients in the same app collision-free. For example, output_dir: "lib/services/network/petstore_client" produces lib/services/network/petstore_client/petstore_client.dart, not generated.dart.
Generated model classes #
Each object schema in components/schemas becomes a final class with:
finalfields for every property (nullable or non-nullable based on therequiredlist)- Named constructor (
constwhen all fields are primitives or enums with noDateTime/List/Map) factory fromJson(Map<String, dynamic> json)— throwsArgumentErrorfor missing required fieldsMap<String, dynamic> toJson()copyWith(...)— nullable fields use an_Undefinedsentinel to distinguishnullfrom "not provided"operator ==andhashCode— list fields use element-by-element comparison; map fields use key-by-key comparison
Example (from the example project's User schema):
final class User {
final String email;
final String id;
final String name;
final UserRole? role;
final DateTime? createdAt;
User({
required this.email,
required this.id,
required this.name,
this.role,
this.createdAt,
});
factory User.fromJson(Map<String, dynamic> json) => User(
email: json['email'] == null
? (throw ArgumentError.notNull('User.email'))
: json['email'] as String,
// ...
);
Map<String, dynamic> toJson() => {
'email': email,
// ...
if (role != null) 'role': role!.toJson(),
};
User copyWith({String? email, /* ... */}) => User(/* ... */);
@override
bool operator ==(Object other) => /* ... */;
@override
int get hashCode => Object.hash(/* ... */);
}
Other schema kinds #
| OpenAPI schema | Generated output |
|---|---|
type: object |
final class with the members above |
type: string/integer/number/boolean (top-level) |
typedef Name = DartType; |
type: array (top-level) |
typedef Name = List<ItemType>; |
enum |
Dart enum with static Name fromJson(T v) and T toJson() methods |
allOf |
Flat-merged final class (properties from all object members combined) |
oneOf |
sealed class parent; each $ref branch stays a standalone class that implements it, declared in the same file as the parent. With a discriminator, fromJson dispatches via switch expression; without one, it tries each variant in turn |
additionalProperties |
Map<String, V> field named additionalProperties |
anyOf is not supported and causes a build error.
Generated service classes #
Each OpenAPI tag produces one class. Operations within a tag are sorted alphabetically by derived method name. Untagged operations go into DefaultApi.
Each method receives:
- Path parameters as positional required arguments (in path-template order)
- Required request body as a positional required argument named
body - Query parameters as named arguments (
requiredor optional matching the spec) - Header parameters as named
String/String?arguments - Dio override parameters:
cancelToken,headers,extra,validateStatus onSendProgress(omitted for GET, HEAD, DELETE, OPTIONS)onReceiveProgress(omitted for HEAD and DELETE)
Return types are derived from the primary 2xx response (200 preferred, then 201, then the lowest 2xx code). Operations with no 2xx response return Future<void>.
Example (from the example project):
class UsersApi {
final Dio _dio;
const UsersApi(this._dio);
Future<User> createUser(
User body, {
CancelToken? cancelToken,
Map<String, dynamic>? headers,
Map<String, dynamic>? extra,
ValidateStatus? validateStatus,
ProgressCallback? onSendProgress,
ProgressCallback? onReceiveProgress,
}) async { /* ... */ }
Future<List<User>> listUsers({
int? page,
CancelToken? cancelToken,
// ...
}) async { /* ... */ }
}
Cookie parameters are not supported. A build warning is emitted and the parameter is omitted. Use Dio interceptors for cookie-based auth.
Generated aggregator class #
The aggregator class (named by client_name) holds a Dio instance and one late final field per tag pointing to the corresponding service class.
class MyApiClient {
late Dio _dio;
static const String _defaultBaseUrl = 'https://api.example.com/v1';
late final UsersApi users = UsersApi(_dio);
// ... one field per tag
MyApiClient({
Dio? dio,
String? baseUrl,
List<Interceptor>? interceptors,
Duration connectTimeout = const Duration(seconds: 30),
Duration receiveTimeout = const Duration(seconds: 30),
}) { /* initializes _dio */ }
}
Auth helper factories #
Static factory methods are generated from components/securitySchemes. Each method returns an Interceptor that you pass via the interceptors constructor parameter.
| Scheme type | Generated factory | Signature |
|---|---|---|
http + scheme: bearer |
bearerAuth |
static Interceptor bearerAuth(String token) |
http + scheme: basic |
basicAuth |
static Interceptor basicAuth(String username, String password) |
apiKey + in: header |
apiKeyAuth |
static Interceptor apiKeyAuth(String apiKey, {String headerName = 'X-Api-Key'}) |
apiKey + in: query |
apiKeyQueryAuth |
static Interceptor apiKeyQueryAuth(String apiKey, {String paramName = 'api_key'}) |
final client = MyApiClient(
interceptors: [
MyApiClient.bearerAuth('your-token'),
],
);
Supported OpenAPI features #
| Feature | Support |
|---|---|
| OpenAPI version | 3.x only (3.0, 3.1, 3.2). Version 2 (Swagger) is rejected. |
| Spec formats | YAML and JSON |
| Spec sources | Local file only — no remote/HTTPS spec URLs |
$ref |
Same-file $ref only (#/components/schemas/...) |
| Primitive types | string, integer, number, boolean |
| Formats | date-time → DateTime (via date_time_converter); all other formats ignored |
type: null |
Parsed as NullSchema; top-level null schemas produce no file |
nullable: true (3.0) |
Supported |
type: [T, 'null'] (3.1) |
Supported |
| Mixed nullable styles | Rejected with a parse error |
enum |
Supported for string, integer, number types |
object with properties |
Supported |
object with additionalProperties |
Supported (bool or typed schema) |
Implicit object (no type, no composition keyword) |
Treated as object |
array with items |
Supported |
allOf |
Flat merge of all object members |
oneOf with discriminator |
Sealed class + switch expression dispatch |
oneOf without discriminator |
Sealed class + fromJson that tries each variant, most specific first |
anyOf |
Not supported; causes a build error |
securitySchemes |
bearer, basic, apiKey (header and query) |
| Path parameters | Supported; URI-encoded via Uri.encodeComponent |
| Query parameters | Supported (required and optional) |
| Header parameters | Supported (required and optional) |
| Cookie parameters | Not supported; build warning emitted |
| Request body | application/json schema resolved to a typed body parameter |
| Response types | Primary 2xx response body (200 preferred) |
| Tags | One service class per tag; untagged ops → DefaultApi |
| OpenAPI 3.2 non-standard verbs | Emitted as stubs using _dio.request(options: Options(method: '...')) |
Caching and rebuilds #
There is no bespoke cache layer — the spec is re-parsed synchronously on every build_runner build invocation (cheap: pure in-memory YAML/JSON parsing), but build_runner's own incremental engine (asset digests) skips re-formatting and re-writing unchanged output, and --delete-conflicting-outputs cleans up stale files. Nothing to configure or delete to force a rebuild — build_runner build always reflects the current spec.
Repository Structure #
dart_openapi_generator/
├── lib/src/
│ ├── builder/ ← OpenApiBuilder (build_runner entry)
│ ├── generator_config.dart ← GeneratorConfig, sourced from build.yaml options
│ ├── date_time_converter.dart ← DateTimeConverter enum
│ ├── parser/ ← OpenAPI 3.x YAML/JSON parser
│ ├── generator/ ← ModelGenerator, ServiceGenerator, AggregatorGenerator
│ ├── writer/ ← FileWriter (format + barrel)
│ └── spec_loader.dart ← readLocalSpec (sync)
├── test/ ← generator test suite
├── example/ ← consumer project exercising current features
│ ├── openapi/example_api.yaml
│ ├── build.yaml ← builder options
│ └── lib/
│ ├── main.dart ← usage example
│ └── generated/ ← regenerated by CI
├── docs/ ← docs.page MDX source
└── pubspec.yaml ← the package itself + pub workspace root (example/ is the only member)
Running Tests #
dart test
End-to-end validation with the example project:
cd example
dart pub get
dart run build_runner build --delete-conflicting-outputs
dart analyze lib/generated/
Example #
The example/ directory in the repository demonstrates a complete setup:
example/openapi/example_api.yaml— the specexample/build.yaml— builder optionsexample/lib/main.dart— usage exampleexample/lib/generated/— the committed generated output
Contributing #
- Fork the repository and create a feature branch.
- Keep
dart analyzeclean anddart formatidempotent. - Add or update tests for any changed behaviour. Run
dart test. - Open a pull request describing what changed and why.
Bug reports and feature requests: issue tracker.
License #
MIT — see LICENSE.