Swagger to Dart
Generates Dart models (freezed) and retrofit API clients from an API description:
- OpenAPI 3.0, 3.1 and 3.2
- Swagger 2.0
- Postman collections: v1, v2.0 and v2.1 exports, and v3 collection directories
- JSON Schema documents (models only)
Input can be JSON or YAML, read from a local file or fetched from a URL. See Inputs.
Requirements
- Dart >= 3.9 (bundled with Flutter >= 3.35) to run
swagger_to_dartitself. - The code it generates uses
json_serializable>= 6.10, which emits Dart 3.8 syntax (null-aware elements) in.g.dartfiles. Your consuming project's own SDK lower bound must be at least3.8.0(e.g.environment: sdk: '>=3.8.0 <4.0.0') or that generated code won't compile. - Generated clients need
dioandretrofit. A spec without operations (a JSON Schema document, or an OpenAPI file with onlycomponents) generates models only; that output needs neitherretrofitnorretrofit_generator, and needsdioonly when a model has a binary (MultipartFile) field.
Support
- FastAPI
- abpIO
- NestJS
- Spring Boot
- AspNet Core
- Flask
- Postman collections
- Any other tool that produces OpenAPI 3.x or Swagger 2.0, in JSON or YAML
Features
- Immutable freezed models with JSON
serialization, including models for inline objects, enums, generics and
sealedunions (also unions that mix strings, numbers, arrays and objects). - retrofit clients, one per tag (one per folder for Postman collections).
- Path, query, header and cookie parameters; JSON, form, multipart, text and binary bodies and responses.
- Documentation comments from the spec's descriptions.
- Fetches the spec from a URL and keeps a local copy.
Getting Started
Add dependencies
First, add the required dependencies to your pubspec.yaml file:
dependencies:
dio: ^x.x.x
retrofit: ^x.x.x
freezed_annotation: ^x.x.x
json_annotation: ^x.x.x
dev_dependencies:
swagger_to_dart: ^x.x.x
build_runner: ^x.x.x
freezed: ^x.x.x
json_serializable: ^x.x.x
retrofit_generator: ^x.x.x
Or use Dart CLI to add the dependencies:
dart pub add dev:swagger_to_dart
dart pub add dev:build_runner
dart pub add freezed_annotation
dart pub add dev:freezed
dart pub add json_annotation
dart pub add dev:json_serializable
dart pub add dio
dart pub add retrofit
dart pub add dev:retrofit_generator
Configure build order
Create a build.yaml file in your project root to ensure the correct build order:
global_options:
freezed:
runs_before:
- json_serializable
json_serializable:
runs_before:
- retrofit_generator
Update the analysis_options.yaml file:
linter:
rules:
prefer_single_quotes: true
analyzer:
exclude:
# for retrofit and json_serializable
- "**/*.g.dart"
# for freezed
- "**/*.freezed.dart"
errors:
# for json_serializable
invalid_annotation_target: ignore
Generate code from your OpenAPI specification
Create a swagger_to_dart.yaml file in your project root:
swagger_to_dart:
input_directory: schema/swagger.json
output_directory: lib/src/gen
model:
support_generic_arguments: true
union_class_fallback_name: fallback
enum_fallback_type: first
api_client:
base_api_client_class_name: BaseApiClient
use_class_for_query_parameters: true
skipped_parameters:
- Language
- X-API-Key
See Configuration Options below for every key. Then run:
dart run swagger_to_dart
The CLI takes one optional flag: --config (short -c) points to a config
file at a different path. It defaults to swagger_to_dart.yaml in the
current directory.
dart run swagger_to_dart [--config path/to/swagger_to_dart.yaml]
Run code generation
After generating the API clients and models, run build_runner to generate the necessary code:
dart run build_runner build
Example Usage
1. Define the OpenAPI specification location
Create a swagger_to_dart.yaml configuration file
2. Generate code
Once you've set up your configuration file, run the following commands:
# First, generate the Dart code from your OpenAPI specification
dart run swagger_to_dart
# Then, generate the implementation with freezed, json_serializable, and retrofit
dart run build_runner build
3. Use the generated code
import 'package:dio/dio.dart';
import 'package:your_project/api/api.dart';
void main() async {
final dio = Dio();
// Add interceptors for auth, logging, etc.
dio.interceptors.add(LogInterceptor(responseBody: true));
// Create API client
final userClient = UserApi(dio, baseUrl: 'https://api.example.com');
try {
// Use the generated API client
final users = await userClient.getUsers();
print('Users: ${users.map((u) => u.name).join(', ')}');
// Create a model instance
final newUser = User(id: '123', name: 'John Doe', email: 'john@example.com');
// Use the model in an API call
final createdUser = await userClient.createUser(newUser);
print('Created user: ${createdUser.name}');
} catch (e) {
print('Error: $e');
}
}
Inputs
input_directory (or url) may point at any of these. The format is
detected from the document's content, checked top to bottom; the first match
wins. Every format is converted to OpenAPI 3 first, then generated the same
way.
| Format | Detected by | Converted to |
|---|---|---|
| OpenAPI 3.0, 3.1, 3.2 | openapi: 3.x |
Used as is |
| Swagger 2.0 | swagger: "2.0" |
OpenAPI 3.0.3 |
| Postman v2.0 / v2.1 export | info.schema is a Postman schema URL |
OpenAPI 3.1.1 (3.2.0 when a request uses QUERY or a non-standard method) |
| Postman v1 export | top-level requests and order |
as above |
| Postman API response | a {"collection": {...}} envelope |
as above |
| Postman v3 collection | input_directory is a directory |
as above |
| JSON Schema | top-level definitions, $defs or $schema |
OpenAPI 3.1.0 with schemas only (no client) |
Anything else fails with Not an OpenAPI 3, Swagger 2.0, JSON Schema or Postman document.
YAML. Files ending in .yaml/.yml, and other files or URL responses
that do not start with { or [, are parsed as YAML, for every format.
Anchors are resolved, and unquoted versions (openapi: 3.1,
swagger: 2.0) are read as strings.
Postman
Point input_directory at a collection export (collection.json) or at a
v3 collection directory (Postman's file-based format:
.resources/definition.yaml, *.request.yaml, *.example.yaml). v1 and v2.0
collections are upgraded to v2.1 first. How a collection maps to OpenAPI:
| Postman | Generated |
|---|---|
| folder | a tag (Parent / Child), so one client per folder (ParentChildClient); requests at the root go to DefaultClient. A folder name without ASCII letters names its client by its requests' common path (/users… gives UsersClient); clashing names get a number (Users2Client) |
| request | one method; the name gives the summary and the method name (camelCase; names without ASCII letters fall back to method + path) |
| URL origin | the most frequent origin (e.g. {{baseUrl}}) is the server; requests on other origins get absolute URLs |
:id / {{id}} path segments |
path parameters |
| query parameters, headers | optional parameters (Content-Type, Accept, Authorization and transport headers are left out; Cookie becomes cookie parameters) |
raw JSON body (comments and {{variables}} allowed) |
a body model inferred from the sample |
| urlencoded / form-data / file / GraphQL body | form model / multipart model (file parts are MultipartFile; unset optional fields are not sent) / binary / {query, variables, operationName} |
| saved examples | responses per status and media type, with models inferred from every example of that response |
| same method and path in several requests | one method; /users/{id} and /users/{userId} count as the same path |
| auth (collection, folder, request) | securitySchemes in the converted spec; generated clients leave auth to your dio interceptors |
| collection variables | resolved in URLs, headers and bodies; values become examples and server defaults |
| scripts, tests, cookies, proxy, certificates | dropped |
- Inferred models: no field is
required, so every field is nullable. Integers mixed with decimals becomedouble. A string becomesDateTimeonly when every sample is an RFC 3339 timestamp. Values of different JSON kinds become a sealed union when one of them is an array or an object, elsedynamic. Objects whose keys are all numbers, UUIDs or dates become aMap. - Secrets: credential values (passwords, tokens, client secrets, API key
values, private keys) are kept out of examples, defaults and descriptions.
Variables holding them (used by auth or
Authorization/Cookieheaders, typedsecret, or named like a credential) stay unresolved{{name}}references; values under credential-named parameters, headers and JSON keys, bearer tokens and JWTs are dropped from examples. Free text (XML or HTML bodies, GraphQL query text, descriptions, values under keys such asaccess) loses only known secret values, the literal values of those auth attributes, headers and variables; a credential written literally only there stays. Identifiers such as usernames and client ids are kept. - v3 directories: gRPC, WebSocket, Socket.IO, MQTT, MCP and LLM requests are skipped with a warning, since they are not HTTP.
- Requests without a saved example have no typed response.
Every property of the official v2.1.0 collection schema is listed, as mapped
or dropped with the reason, in the converter's
coverage table;
a test fails when the schema has a property the table does not list. The
converter is also usable on its own: postmanToOpenApi in
postman_collection.
Swagger 2.0
Converted to OpenAPI 3.0.3 the way
swagger2openapi does: definitions,
parameters and responses move to components (refs follow; keys like
Page«Pet» become valid names such as Page_Pet_), body and formData
parameters become request bodies (type: file is binary), consumes/
produces become media types, host + basePath + schemes become
servers, securityDefinitions become securitySchemes, x-nullable
becomes nullable, and collectionFormat becomes style/explode
(tsv has no OpenAPI 3 equivalent and is kept as x-collectionFormat).
JSON Schema
A JSON Schema document (any draft) generates models only. Each entry of
definitions or $defs becomes a model named by its key (definition
titles are ignored). The root schema becomes a model named by its title,
else by the input file name (collection.json gives Collection), unless
it only holds definitions. #/definitions/…, #/$defs/… and # refs are
rewritten. Postman's own collection schema is generated this way into
postman_collection's models.
Configuration Options
The package configuration is defined in a swagger_to_dart.yaml file. Every
key, with its default:
swagger_to_dart:
# Fetch the spec (JSON or YAML, any supported format) from a URL instead
# of only reading `input_directory`. On success, `input_directory` is
# overwritten with the fetched document as pretty-printed JSON so it stays
# a fresh local copy. On failure, generation
# falls back to the existing local copy with a loud console warning
# naming its age; with no local copy either, generation fails.
# Default: unset (read `input_directory` only).
url: https://api.example.com/openapi.json
# The input: a JSON or YAML file in any supported format (see Inputs), or
# a Postman v3 collection directory. Read directly when `url` is unset,
# and used as the fetch/refresh target when it is set.
input_directory: schema/swagger.json # default
# Where generated models and API clients are written.
output_directory: lib/src/gen # default
# Backend that produced the spec: FastAPI | dotnet | abp.io. Selects the
# generic-type-name parser (e.g. `PagedResultDto<UserDto>`-style naming
# differs per framework) and, in Flutter projects, which string `format`s
# map to native types instead of `String` (color / color-hex -> Color,
# FastAPI only; time / duration -> TimeOfDay, FastAPI only).
# Default: unset.
generation_source: FastAPI
imports:
# Raw import statements prepended verbatim to every generated file.
# Default: [].
global:
- "import 'package:my_app/interceptors.dart';"
model:
# Generate a real generic Dart class (`class Foo<T>`, with a generic
# `fromJson`) for a component schema whose title matches
# `generation_source`'s generic-instantiation naming convention,
# instead of one flat class per instantiation. Default: false.
support_generic_arguments: false
# Name of the fallback variant for a oneOf/anyOf union whose payload
# matches no variant (bad/missing discriminator value, or nothing
# decodes without error). The fallback wraps the raw
# `Map<String, dynamic>`. Default: unset — an unmatched payload throws
# `ArgumentError` instead.
union_class_fallback_name: fallback
# What an unrecognized value decodes to. Default: throwException.
# throwException - throws (also what happens at runtime when unset).
# unknown - adds a real `unknown` member (reusing one if the
# enum already declares it; int enums get a JSON
# value below the real ones, e.g. -1) and decodes
# unrecognized values to it.
# first / last - decodes to the first / last declared member.
enum_fallback_type: throwException
# Prefixes stripped from a schema's name/title before it becomes a Dart
# class name (longest match wins), e.g. ["Api"] turns `ApiUserDto` into
# `UserDto`. Applied consistently to regular, enum, union and generic
# models and their file names. Default: [].
remove_model_prefixes: []
# Prepended once to every generated model name: component and inline
# classes, enums, unions and typedefs, and their file names (`Postman`
# turns `Item` into `PostmanItem` in `postman_item.dart`). Core types
# such as `String` or `List` are never prefixed. Must be an ASCII
# capital letter followed by ASCII letters or digits. Default: unset.
class_prefix: Postman
# Opt-in per-enum member renaming, keyed by the enum's schema name or
# generated Dart class name; the inner map is the raw enum value (as a
# string — works for string and integer enums) to the desired Dart
# member name (recased to camelCase). Enums absent here keep their
# default member names (e.g. `value0`). Default: {}.
enums:
MyStatusEnum:
0: created
10: pgRegistered
api_client:
# Class name of the shared base client every generated client uses.
base_api_client_class_name: BaseApiClient # default
# Bundle an operation's query parameters into one generated
# `<Method>QueryParameters` class, passed as a single `@Queries()`
# argument, instead of one named method parameter per query field.
# Default: false.
use_class_for_query_parameters: false
# Operation parameters dropped entirely from the generated method
# signature (and so never sent) — e.g. ones your own Dio interceptors
# already inject, like `Language` or `X-API-Key`. Default: [].
skipped_parameters:
- Language
- X-API-Key
# Default each generated method's `@Extras()` to that operation's
# OpenAPI metadata (tags, operationId, parameters, responses), readable
# via `options.extra` in a Dio interceptor. `example`/`examples` (and
# Swagger 2.0 `x-example`/`x-examples`) are left out, so sample data is
# not compiled into the app. Set to `false` to stop embedding it.
# Default: true.
include_openapi_extras: true
Generated models
- Inline objects (a schema with
propertiesthat is not a component) become models named by where they are used: the response ofgetUserisGetUserResponse, the body ofupdateUserisUpdateUserBody, theaddressproperty ofGetUserResponseisGetUserResponseAddress, array items end inItemand map values inValue. Atitlenames the model instead, unless a component, another model or adart:core/dio type already has that name. Inline objects used as parameters, and objects withoutproperties, stayMap<String, dynamic>. - Components that are not objects (arrays, maps, primitives and
$refaliases) become typedefs, e.g.typedef Pets = List<Pet>;. A typedef that would refer to itself (Tree = List<Tree>) types that referenceObject?instead, with a warning. - Names that clash with a type dio, retrofit or the generated code uses
(
Response,Method,List,Headers,HttpResponse) get a number (Response2), as do union case classes a component already names. - Names without ASCII letters (Arabic, emoji): such components are
Schema,Schema2, … (with a warning; give them atitle), fields areempty,empty2, … with nested models named after them (UserEmpty), enum values arevalue1,value2, … by position, and methods are named by HTTP method and path (getPeopleId). Clients of tags starting with digits are named from the first letter (1. Auth→AuthClient). model.class_prefixprefixes every model name once (see Configuration Options).
Unions
A component schema that is a oneOf/anyOf of $refs generates a sealed
class with one final subclass per variant, plus a fallback variant when
model.union_class_fallback_name is set. fromJson/toJson work on the
variant's own flat JSON — there is no {"value": ...} envelope — so a union
decodes the same way whether it's a field, a list item, a request body or a
response:
- With a discriminator: decoding switches on the discriminator property (variants the mapping leaves out, or all of them when it is omitted, use their schema name as the value); encoding writes the discriminator back.
- Without one: among the variants whose required keys are all present, the one declaring the most of the payload's keys wins (the earlier one on a tie).
- No match: the fallback variant wraps the raw
Map<String, dynamic>; with nounion_class_fallback_nameconfigured, decoding throwsArgumentErrorinstead.
final Animal animal = Animal.fromJson(json); // AnimalDog, AnimalCat, or the fallback
final label = switch (animal) {
AnimalDog(:final value) => 'Dog: ${value.name}',
AnimalCat(:final value) => 'Cat: ${value.name}',
AnimalFallback(:final value) => 'Unknown animal: $value',
};
print(animal.toJson()); // flat JSON, discriminator included
Mixed unions
A oneOf/anyOf, or a type array such as [string, object], that mixes
JSON kinds with at least one array or object also generates a sealed
class. Its fromJson(Object? json) switches on the JSON kind first, then
on the discriminator or keys among object variants. Cases are named
string, integer, number, boolean, list, object, or after the
referenced schema; an integer and a number variant merge into one
number case (double). Object variants that each pin one const property to a different value
are discriminated by it.
final Url url = Url.fromJson(json); // a String or an object
final raw = switch (url) {
UrlString(:final value) => value,
UrlObject(:final value) => value.raw,
};
Variants of one primitive kind are that type ([enum, string] is a
String, [integer, number] a double); mixes of different primitive
kinds (string | integer) stay dynamic. Mixed unions
used as parameters or request bodies are dynamic, and a list or map of
them as a response is List<Object?> / Map<String, Object?> (decode the
items with Url.fromJson).
Handling Breaking Changes
When your API changes, you can use the following workflow to update your generated code:
- Update your OpenAPI specification
- Run
dart run swagger_to_dart - Run
dart run build_runner build - Check for breaking changes in your codebase and update as needed
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
This repository is test-driven: every behaviour change starts as a failing test, and CI blocks anything that isn't green. See CONTRIBUTING.md for setup, the package layout and the red-green loop.
CI/CD
Publish new release https://github.com/masreplay/api_spec_to_dart/actions
make publish
License
This package is available under the MIT License.
Acknowledgements
This package was inspired by and builds upon other great Dart packages including freezed, retrofit, and json_serializable and FastAPI.