Generated Code

Every file type generated by dart_openapi_generator — structure, naming, and real examples.

Running dart run build_runner build writes the following file tree into your outputDir (e.g. lib/generated/). Every file starts with a // GENERATED CODE - DO NOT MODIFY BY HAND header and a // @dart=3.0 directive.

lib/generated/
├── api_client.dart       ← aggregator class (named via clientName)
├── generated.dart        ← barrel: exports everything alphabetically
├── models/
│   └── *.dart            ← one file per schema (alphabetical)
└── services/
    └── *_api.dart        ← one file per OpenAPI tag (alphabetical)

Ordering

All generated output is deterministically alphabetical:

  • Models in models/ ordered by schema name
  • Services in services/ ordered by tag name
  • Properties within each class ordered by field name
  • Imports within each file ordered alphabetically
  • The generated.dart barrel exports in alphabetical order

Regenerating the same spec twice always produces byte-identical output — no diff noise in version control.

Barrel: generated.dart

One import gives access to every generated type:

lib/main.dart
import 'generated/generated.dart';

The barrel content from the example project:

lib/generated/generated.dart
// GENERATED CODE - DO NOT MODIFY BY HAND
// @dart=3.0
// Generated by dart_openapi_generator

export 'api_client.dart';
export 'models/category.dart';
export 'models/metadata.dart';
export 'models/notification.dart';
export 'models/token_request.dart';
export 'models/token_response.dart';
export 'models/user.dart';
export 'models/user_profile.dart';
export 'models/user_role.dart';
export 'models/user_search_request.dart';
export 'services/auth_api.dart';
export 'services/categories_api.dart';
export 'services/debug_api.dart';
export 'services/users_api.dart';

Model: Plain Object

For an OpenAPI object schema the generator emits a final class with:

  • Alphabetically ordered final fields
  • Named constructor (required this.field for required fields, this.field for optional)
  • factory fromJson(Map<String, dynamic> json) — throws ArgumentError.notNull on missing required fields
  • Map<String, dynamic> toJson() — omits null optional fields with if guards
  • copyWith — uses an _Undefined sentinel for nullable fields to distinguish "not provided" from null
  • operator == and hashCode — deep equality, uses Object.hash

Verbatim from example/lib/generated/models/user.dart:

lib/generated/models/user.dart
// GENERATED CODE - DO NOT MODIFY BY HAND
// @dart=3.0
// Generated by dart_openapi_generator

import 'user_role.dart';

class _Undefined {
  const _Undefined();
}

const _Undefined _undefined = _Undefined();

final class User {
  final String? class_;
  final DateTime? createdAt;
  final String? default_;
  final String email;
  final String id;
  final String name;
  final UserRole? role;

  User({
    this.class_,
    this.createdAt,
    this.default_,
    required this.email,
    required this.id,
    required this.name,
    this.role,
  });

  factory User.fromJson(Map<String, dynamic> json) => User(
        class_: json['class'] == null ? null : json['class'] as String,
        createdAt: json['createdAt'] == null
            ? null
            : DateTime.parse(json['createdAt'] as String),
        default_: json['default'] == null ? null : json['default'] as String,
        email: json['email'] == null
            ? (throw ArgumentError.notNull('User.email'))
            : json['email'] as String,
        id: json['id'] == null
            ? (throw ArgumentError.notNull('User.id'))
            : json['id'] as String,
        name: json['name'] == null
            ? (throw ArgumentError.notNull('User.name'))
            : json['name'] as String,
        role: json['role'] == null
            ? null
            : UserRole.fromJson(json['role'] as String),
      );

  Map<String, dynamic> toJson() => {
        if (class_ != null) 'class': class_,
        if (createdAt != null) 'createdAt': createdAt!.toIso8601String(),
        if (default_ != null) 'default': default_,
        'email': email,
        'id': id,
        'name': name,
        if (role != null) 'role': role!.toJson(),
      };

  User copyWith({
    Object? class_ = _undefined,
    Object? createdAt = _undefined,
    Object? default_ = _undefined,
    String? email,
    String? id,
    String? name,
    Object? role = _undefined,
  }) =>
      User(
        class_: identical(class_, _undefined) ? this.class_ : class_ as String?,
        createdAt: identical(createdAt, _undefined)
            ? this.createdAt
            : createdAt as DateTime?,
        default_: identical(default_, _undefined)
            ? this.default_
            : default_ as String?,
        email: email ?? this.email,
        id: id ?? this.id,
        name: name ?? this.name,
        role: identical(role, _undefined) ? this.role : role as UserRole?,
      );

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is User &&
          class_ == other.class_ &&
          createdAt == other.createdAt &&
          default_ == other.default_ &&
          email == other.email &&
          id == other.id &&
          name == other.name &&
          role == other.role;

  @override
  int get hashCode => Object.hash(
        class_,
        createdAt,
        default_,
        email,
        id,
        name,
        role,
      );
}

Notes:

  • Fields class_ and default_ show reserved-word escaping. Dart reserved words get a _ suffix. The JSON key 'class' maps to the Dart field class_.
  • createdAt uses DateTime.parse because dateTimeConverter defaults to iso8601. Use DateTimeConverter.timestamp for millisecond epoch integers.
  • The _Undefined sentinel in copyWith allows passing null explicitly to clear a nullable field — without it, null would be indistinguishable from "leave unchanged".

Model: Enum

For an OpenAPI enum schema the generator emits a Dart enum with fromJson/toJson switch expressions. String, integer, and number enum types are all handled.

lib/generated/models/user_role.dart
// GENERATED CODE - DO NOT MODIFY BY HAND
// @dart=3.0
// Generated by dart_openapi_generator

enum UserRole {
  admin,
  member,
  viewer;

  static UserRole fromJson(String v) => switch (v) {
        'admin' => UserRole.admin,
        'member' => UserRole.member,
        'viewer' => UserRole.viewer,
        final t => throw ArgumentError('Unknown UserRole value: $t'),
      };

  String toJson() => switch (this) {
        UserRole.admin => 'admin',
        UserRole.member => 'member',
        UserRole.viewer => 'viewer',
      };
}

Enum values are sorted alphabetically by their sanitized Dart identifier. Wire values with special characters (spaces, hyphens, leading digits) are sanitized to valid camelCase Dart identifiers. If sanitization produces a duplicate identifier, generation fails with a clear error asking you to rename the conflicting values in the spec.

Model: allOf

For an OpenAPI allOf schema, the generator merges all object members into a single flat final class. Properties from all members are collected, duplicates are resolved by taking the first occurrence (a warning is emitted when types conflict), and required lists are unioned. The resulting class has the same structure as a plain object class.

Model: oneOf with Discriminator

For an OpenAPI oneOf schema with a discriminator, the generator emits a sealed class parent and one final class per variant. All variants are inlined into the single parent file — no separate files per variant.

The parent's factory fromJson checks that the discriminator key is present, then dispatches to the correct variant using a switch expression:

lib/generated/models/notification.dart
// GENERATED CODE - DO NOT MODIFY BY HAND
// @dart=3.0
// Generated by dart_openapi_generator

sealed class Notification {
  const Notification();

  factory Notification.fromJson(Map<String, dynamic> json) {
    if (!json.containsKey('type')) {
      throw ArgumentError('Missing discriminator key "type" in JSON');
    }
    return switch (json['type']!.toString()) {
      'email' => EmailNotification.fromJson(json),
      'push' => PushNotification.fromJson(json),
      final t => throw ArgumentError(
          'Unknown Notification discriminator value: $t (key: type)'),
    };
  }

  Map<String, dynamic> toJson();
}

final class EmailNotification extends Notification {
  final String email;
  final String type;

  const EmailNotification({required this.email, required this.type});

  factory EmailNotification.fromJson(Map<String, dynamic> json) =>
      EmailNotification(
        email: json['email'] == null
            ? (throw ArgumentError.notNull('EmailNotification.email'))
            : json['email'] as String,
        type: json['type'] == null
            ? (throw ArgumentError.notNull('EmailNotification.type'))
            : json['type'] as String,
      );

  @override
  Map<String, dynamic> toJson() => {'email': email, 'type': type};

  EmailNotification copyWith({String? email, String? type}) =>
      EmailNotification(
        email: email ?? this.email,
        type: type ?? this.type,
      );

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is EmailNotification &&
          email == other.email &&
          type == other.type;

  @override
  int get hashCode => Object.hash(email, type);
}

// PushNotification follows the same pattern ...

Sealed classes enable exhaustive switch in your app code. Add a new variant to the spec and regenerate — the Dart compiler will catch any unhandled case in existing switch expressions.

A oneOf schema without a discriminator produces a sealed class whose fromJson always throws UnimplementedError. Add a discriminator to the spec to get a working fromJson.

Service

For each OpenAPI tag, the generator emits one file services/{tag}_api.dart containing class {Tag}Api. Every method:

  • Is named from operationId (lowerCamelCase), falling back to {method}{PathSegments} when no operationId is set
  • Accepts Dio override parameters: cancelToken, headers, extra, validateStatus, and where applicable onSendProgress / onReceiveProgress
  • Returns Future<void> when there is no 2xx response schema, Future<List<T>> for array responses, Future<T> for object responses
  • Throws DioException on non-2xx responses (standard Dio behavior)
  • Documents Throws [DioException] on non-2xx response. in its dartdoc
  • URL-encodes path parameters with Uri.encodeComponent

Verbatim listUsers method from example/lib/generated/services/users_api.dart:

lib/generated/services/users_api.dart
class UsersApi {
  final Dio _dio;

  const UsersApi(this._dio);

  /// List all users
  ///
  /// Throws [DioException] on non-2xx response.
  Future<List<User>> listUsers({
    int? page,
    CancelToken? cancelToken,
    Map<String, dynamic>? headers,
    Map<String, dynamic>? extra,
    ValidateStatus? validateStatus,
    ProgressCallback? onReceiveProgress,
  }) async {
    final response = await _dio.get<List<dynamic>>(
      '/users',
      queryParameters: {
        if (page != null) 'page': page,
      },
      options: Options(
          headers: headers, extra: extra, validateStatus: validateStatus),
      cancelToken: cancelToken,
      onReceiveProgress: onReceiveProgress,
    );
    final data = response.data;
    if (data == null) {
      throw StateError('Expected JSON list response body but received null.');
    }
    return data.map((e) => User.fromJson(e as Map<String, dynamic>)).toList();
  }
}

Parameter handling:

  • Path parameters become positional required parameters, ordered as they appear in the URL template
  • Required query/header parameters become named required parameters
  • Optional query/header parameters become named optional (T?) parameters
  • cookie parameters are not emitted — a warning is logged, and the dartdoc recommends Dio interceptors instead
  • Header parameters are merged with the caller-supplied headers map at runtime

Aggregator: api_client.dart

The aggregator is named via clientName (default: 'ApiClient'). It holds one late final service field per tag and static auth factories generated from securitySchemes.

Constructor parameters:

ParameterTypeDescription
dioDio?Inject a pre-configured Dio instance. If null, a new instance is created.
baseUrlString?Override the base URL. Defaults to servers[0].url from the spec.
interceptorsList<Interceptor>?Dio interceptors added to the internal Dio instance.
connectTimeoutDurationDefault: Duration(seconds: 30).
receiveTimeoutDurationDefault: Duration(seconds: 30).

Auth factories are generated based on securitySchemes in the spec:

Scheme typeFactory method
type: http, scheme: bearerstatic Interceptor bearerAuth(String token)
type: http, scheme: basicstatic Interceptor basicAuth(String username, String password)
type: apiKey, in: headerstatic Interceptor apiKeyAuth(String apiKey, {String headerName})
type: apiKey, in: querystatic Interceptor apiKeyQueryAuth(String apiKey, {String paramName})

See Advanced — Authentication for usage examples of each factory.

See also: Annotation Reference · Advanced