Skip to content

PurpleSoftSrl/openapi_flutter_gen

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

48 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

openapi_flutter_gen

High-performance OpenAPI-to-Dart/Flutter code generator. Parse your spec once, get a complete, ready-to-use API client with zero build_runner, zero code generation at build time.

pub.dev CI Publish Stars License tests


Why this exists

Most Dart/Flutter OpenAPI generators require build_runner to run in YOUR project, every time the spec changes. They generate .g.dart files you must commit and maintain, slowing your builds and coupling your app to code-gen tooling.

openapi_flutter_gen runs once as a standalone CLI. It produces standalone .dart source files — immutable models, typed API services, sealed exhaustive responses, auth interceptors, pagination helpers. Commit them, import them, done.

Comparison with real OpenAPI generators on pub.dev

Features verified by cloning and analyzing each competitor's source code (June 2026).

Feature openapi_flutter_gen swagger_dart_code_generator swagger_parser openapi_generator space_gen openapi_spec
Zero build_runner in consumer
Sealed exhaustive responses
Immutable models (const)
Typed auth from spec
Multipart/FormData
Pagination helpers
Isolate JSON deserialization
Swagger 2.0 support
oneOf / allOf / anyOf
copyWith / == / hashCode
Standalone CLI (no host project)

Install

dart pub global activate openapi_flutter_gen

Or add to your project's dev_dependencies:

dev_dependencies:
  openapi_flutter_gen: ^0.2.10

Run

openapi_flutter_gen --spec swagger.json --output ./lib/api --package-name my_api

Or from a URL:

openapi_flutter_gen --spec-url https://api.example.com/swagger/v1/swagger.json -o ./lib/api

All flags

-s, --spec          Path to OpenAPI spec (JSON or YAML)
-u, --spec-url      URL to OpenAPI spec
-o, --output        Output directory (default: ./generated)
-p, --package-name  Dart package name (default: api_client)
--use-compute       Generate Isolate.run wrappers for JSON deserialization
--no-isolates       Disable parallel generation
-h, --help          Show usage

Generated client structure

my_api/
├── pubspec.yaml                    # Dio + collection dependencies
├── analysis_options.yaml           # Lint rules
└── lib/
    ├── my_api.dart                 # Barrel export
    └── src/
        ├── models/                 # One file per schema
        │   ├── pet.dart            # Immutable class: fromJson, toJson, copyWith, ==, hashCode
        │   ├── pet_status.dart     # Enum with fromJson/toJson
        │   └── ...
        ├── api/                    # Services + result types
        │   ├── api_client.dart     # Root client with typed service getters
        │   ├── pets_api.dart       # PetsApi: one method per operation
        │   ├── list_pets_result.dart  # Sealed result for each operation
        │   └── ...
        └── core/                   # Support files
            ├── auth.dart           # Typed security (Bearer, ApiKey, OAuth2)
            ├── error_handler.dart  # ApiErrorInterceptor (global + per-call)
            ├── interceptors.dart   # Auth, Retry, Logging
            └── pagination.dart     # Offset + Cursor pagination

Generated code walkthrough

Models — immutable, const, full-featured

Each schema becomes a standalone class with everything you need:

class Pet {
  const Pet({
    required this.id,
    required this.name,
    this.tag,
    this.status,
  });

  final int id;
  final String name;
  final String? tag;
  final PetStatus? status;

  // Deserialization: single hash lookup per field, null-safe
  factory Pet.fromJson(Map<String, dynamic> json) {
    return Pet(
      id: (json['id'] as num).toInt(),
      name: json['name'] as String,
      tag: json['tag'] != null ? json['tag'] as String : null,
      status: json['status'] != null ? PetStatus.fromJson(json['status'] as String) : null,
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      if (tag != null) 'tag': tag,
      if (status != null) 'status': status.toJson(),
    };
  }

  Pet copyWith({int? id, String? name, String? tag, PetStatus? status}) {
    if (id == null && name == null && tag == null && status == null) return this;
    return Pet(
      id: id ?? this.id,
      name: name ?? this.name,
      tag: tag ?? this.tag,
      status: status ?? this.status,
    );
  }

  @override
  bool operator ==(Object other) =>
      identical(this, other) || (other is Pet && id == other.id && name == other.name && tag == other.tag && status == other.status);

  @override
  int get hashCode => Object.hash(id, name, tag, status);

  @override
  String toString() => 'Pet(id=$id, name=$name, tag=$tag, status=$status)';
}

Enums — string or int-backed

enum PetStatus {
  available('available'),
  pending('pending'),
  sold('sold');

  const PetStatus(this.value);
  final String value;

  static PetStatus fromJson(String json) =>
      PetStatus.values.firstWhere((e) => e.name == json || e.value == json,
          orElse: () => throw ArgumentError('Unknown PetStatus: $json'));

  String toJson() => value;
}

Sealed union types — oneOf / anyOf

oneOf schemas become sealed class hierarchies with exhaustive pattern matching:

sealed class PetType {
  const PetType();

  factory PetType.fromJson(Map<String, dynamic> json) {
    if (json.containsKey('petType')) {
      return switch (json['petType'] as String) {
        'dog' => PetTypeDog.fromJson(json),
        'cat' => PetTypeCat.fromJson(json),
        _ => _PetTypeUnknown.fromJson(json),
      };
    }
    try { return PetTypeDog.fromJson(json); } catch (_) {}
    try { return PetTypeCat.fromJson(json); } catch (_) {}
    throw FormatException('Cannot decode PetType', json);
  }
}

class PetTypeDog extends PetType {
  const PetTypeDog(this.value);
  final Dog value;
  factory PetTypeDog.fromJson(Map<String, dynamic> json) => PetTypeDog(Dog.fromJson(json));
  Map<String, dynamic> toJson() => value.toJson();
}

class PetTypeCat extends PetType {
  const PetTypeCat(this.value);
  final Cat value;
  factory PetTypeCat.fromJson(Map<String, dynamic> json) => PetTypeCat(Cat.fromJson(json));
  Map<String, dynamic> toJson() => value.toJson();
}

API services — one method per operation, typed parameters

class PetsApi {
  const PetsApi({required this.dio, this.baseUrl});
  final Dio dio;
  final String? baseUrl;

  Future<ListPetsResult> listPets({
    int? limit,
    PetStatus? status,
    CancelToken? cancelToken,
    Map<String, dynamic>? extra,
    Options? options,
  }) async {
    final reqQueryParams = <String, dynamic>{};
    if (limit != null) reqQueryParams['limit'] = limit.toString();
    if (status != null) reqQueryParams['status'] = status.toJson().toString();
    final response = await dio.request(
      '/pets',
      queryParameters: reqQueryParams.isNotEmpty ? reqQueryParams : null,
      options: options ?? Options(method: 'GET', extra: extra),
      cancelToken: cancelToken,
    );
    return ListPetsResult.fromResponse(response);
  }

  Future<CreatePetResult> createPet({
    required CreatePetRequest createPetRequest,
    CancelToken? cancelToken,
    Map<String, dynamic>? extra,
    Options? options,
  }) async {
    final response = await dio.request(
      '/pets',
      data: createPetRequest.toJson(),
      options: options ?? Options(method: 'POST', extra: extra),
      cancelToken: cancelToken,
    );
    return CreatePetResult.fromResponse(response);
  }
}

Path parameters are automatically interpolated:

Future<GetPetResult> getPet({
    required int petId,
    ...
}) async {
    final response = await dio.request('/pets/$petId', ...);
    ...
}

Sealed result types — every HTTP status code is a typed variant

sealed class ListPetsResult {
  const ListPetsResult();

  factory ListPetsResult.fromResponse(Response response) {
    return switch (response.statusCode!) {
      200 => ListPetsResultHttp200(
        List<Pet>.generate(response.data.length, (i) => Pet.fromJson((response.data as List)[i]), growable: false),
      ),
      400 => ListPetsResultHttp400(Error.fromJson(response.data as Map<String, dynamic>)),
      _ => ListPetsResultError.fromResponse(response),
    };
  }
}

class ListPetsResultHttp200 extends ListPetsResult {
  const ListPetsResultHttp200(this.data);
  final List<Pet> data;
}

class ListPetsResultHttp400 extends ListPetsResult {
  const ListPetsResultHttp400(this.data);
  final Error data;
}

class ListPetsResultError extends ListPetsResult {
  const ListPetsResultError(this.response);
  final Response<dynamic> response;

  factory ListPetsResultError.fromResponse(Response response) => ListPetsResultError(response);
}

Using the generated client

1. Import and initialize

import 'package:my_api/my_api.dart';

final client = ApiClient(
  baseUrl: 'https://api.example.com',
  bearerAuth: BearerAuthSecurity(token: jwt),
  errorHandler: ApiErrorInterceptor(
    onUnauthorized: (_) => logout(),
    onServerError: (_) => showErrorToast(),
  ),
);

2. Make API calls with exhaustive switch

final result = await client.pets.listPets(limit: 20);

switch (result) {
  case ListPetsResultHttp200(:final data):
    print('Got ${data.length} pets: ${data.map((p) => p.name).join(", ")}');
  case ListPetsResultHttp400(:final data):
    print('Bad request: ${data.message}');
  case ListPetsResultError(:final response):
    print('HTTP ${response.statusCode}: unexpected error');
}

3. Create resources

final result = await client.pets.createPet(
  createPetRequest: CreatePetRequest(
    name: 'Rex',
    status: PetStatus.available,
  ),
);

switch (result) {
  case CreatePetResultHttp201(:final data):
    print('Created pet ${data.id}: ${data.name}');
  case CreatePetResultError(:final response):
    print('Failed: ${response.statusCode}');
}

4. Upload files (multipart/form-data)

Multipart endpoints automatically generate FormData — no manual construction needed:

final result = await client.mediaManager.addOrUpdateMedia(
  culture: 'it',
  bodyMultipartFormData: AddOrUpdateMediaBodyMultipartFormData(
    mediaFile: imageBytes,     // Uint8List → MultipartFile.fromBytes automatically
    contentId: '12345',
    contentTypeId: 'image',
  ),
);

5. Download binary files

Binary responses use ResponseType.bytes automatically:

final result = await client.mediaManager.getMedia(mediaId: 'abc');
switch (result) {
  case GetMediaResultHttp200(:final data):
    // data is Uint8List
    await File('downloaded.pdf').writeAsBytes(data);
}

6. Error handling — global + per-call

Global error handler catches all calls:

final client = ApiClient(
  errorHandler: ApiErrorInterceptor(
    onUnauthorized: (_) => redirectToLogin(),
    onServerError: (_) => reportCrash(),
  ),
);

Per-call override with chain or skip:

// Chain: per-call runs first, then global
await client.pets.deletePet(
  petId: 123,
  extra: {
    'perCallErrorHandler': ApiErrorInterceptor(
      onNotFound: (_) => showToast('Already deleted'),
    ),
  },
);

// Skip global — only this handler fires
await client.pets.deletePet(
  petId: 123,
  extra: {
    'perCallErrorHandler': ApiErrorInterceptor(
      skipGlobal: true,
      onNotFound: (_) => showToast('Already deleted'),
    ),
  },
);

7. Pagination — forEach / toList

Offset-based:

final result = await client.pets.listPets(limit: 50);
switch (result) {
  case ListPetsResultHttp200(:final data):
    await data.forEach((pet) async => await processPet(pet));
    final all = await data.toList(); // collects all pages
}

8. Dio cache integration

Add dio_mcache for transparent caching:

import 'package:dio_mcache/dio_mcache.dart';

final client = ApiClient(
  baseUrl: 'https://api.example.com',
  bearerAuth: BearerAuthSecurity(token: jwt),
  dio: Dio()..interceptors.add(DioCacheInterceptor(
    options: DioCacheOptions(expiration: const Duration(minutes: 5)),
  )),
);
// All API calls are now cached automatically

9. Auth — token provider for dynamic refresh

final client = ApiClient(
  bearerAuth: BearerAuthSecurity(
    tokenProvider: () async {
      final stored = await secureStorage.read('jwt');
      if (stored != null) return stored;
      return await refreshToken();
    },
  ),
);

10. Custom Dio instance

final client = ApiClient(
  dio: Dio(BaseOptions(
    baseUrl: 'https://api.example.com',
    connectTimeout: const Duration(seconds: 10),
    receiveTimeout: const Duration(seconds: 30),
  ))..interceptors.addAll([
    DioCacheInterceptor(options: DioCacheOptions(expiration: const Duration(minutes: 5))),
    LoggingInterceptor(),
  ]),
);

Compute mode — Isolate-based JSON deserialization

For large API responses, JSON deserialization can block the main isolate. Use --use-compute to generate Isolate.run wrappers:

openapi_flutter_gen --spec swagger.json --use-compute

The generated service methods wrap deserialization in an isolate:

Future<ListPetsResult> listPets({...}) async {
  final response = await dio.request('/pets', ...);
  // Deserialization runs in a separate isolate — main thread stays responsive
  final statusCode = response.statusCode ?? 0;
  final data = response.data;
  return Isolate.run(() => deserializeListPets((statusCode: statusCode, data: data)));
}

Supported specifications

Format Version Tested with
OpenAPI 3.x (JSON) 3.0.x petstore.json (14 schemas)
OpenAPI 3.x (YAML) 3.1.x train-travel OpenAPI (47 schemas)
Swagger 2.0 (JSON) 2.0 petstore.swagger.io (16 schemas)
Production API (JSON) 3.0 927 schemas, 605 operations, 0 issues

Features

  • OAS 3.x + Swagger 2.0 — JSON + YAML, full $ref resolution, oneOf/anyOf/allOf, discriminators, inline schema extraction at any nesting depth
  • Sealed exhaustive responses — every HTTP status code maps to a typed variant; switch statements are checked for exhaustiveness at compile time
  • Immutable modelsconst constructors, copyWith with zero-allocation fast path, structural ==/hashCode, toString
  • Typed auth — Bearer, ApiKey, OAuth2, OpenID Connect generated from spec's securitySchemes; static token or dynamic tokenProvider
  • Per-request auth override — every API method accepts options and extra parameters, enabling per-call cache control, error handling, and more
  • Multipart/FormData — binary fields automatically use MultipartFile.fromBytes in toFormData(); string/int fields map to form fields
  • Error handling — global ApiErrorInterceptor with callbacks per status code; per-call overrides with chain/skip semantics
  • Pagination — offset-based and cursor-based PaginatedResponse<T> with forEach and toList extensions
  • Dio-based — uses Dio for HTTP; supports custom Dio instances, interceptors, and base URL
  • Compute mode--use-compute wraps heavy JSON deserialization in Isolate.run, keeping the main isolate responsive
  • Parallel generation — file writing distributed across isolates for large specs
  • Zero build_runner — no code generation at build time; no .g.dart files; just import and use

Architecture

Spec (JSON/YAML)
    │
    ▼
Loader ──► SwaggerNormalizer (Swagger 2.0 → OAS 3.x)
    │
    ▼
OpenApiSpecParser
    ├── $ref resolution
    ├── inline schema extraction (recursive, any depth)
    └── IR construction (IrSchema, IrOperation, IrApiDocument)
    │
    ▼
CodeGenerator (parallel via Isolate.spawn)
    ├── ModelGenerator     (IrObjectSchema → class, IrEnumSchema → enum, IrUnionSchema → sealed class)
    ├── ApiGenerator       (IrOperation → service method + sealed result type)
    └── SupportGenerator   (auth, error handler, interceptors, pagination, pubspec, barrel)
    │
    ▼
.dart files → import and use

Contributing

dart test  # 36 tests, 3 spec formats, compute + normal mode

License

Dual-licensed.

Open Source — GNU AGPL v3

You can use, modify, and distribute this software freely under the terms of the GNU Affero General Public License v3. This includes the network-use clause: if you modify openapi_flutter_gen and run it as part of a network service (SaaS), you must make your modifications available to users of that service.

Commercial License

If the AGPL does not fit your business model, a commercial license is available.

What you get:

  • Full rights to use openapi_flutter_gen in proprietary, closed-source applications
  • No obligation to disclose your source code or modifications
  • No network-use copyleft restrictions
  • Priority email support
  • Indemnification

Contact us for pricing and terms.

About

High-performance OpenAPI-to-Dart/Flutter code generator. Immutable models, sealed exhaustive response types, typed auth, multipart FormData, pagination, Isolate-based JSON deserialization. Zero build_runner.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages