Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

180 changes: 180 additions & 0 deletions docs/content/docs/guides/dart.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
---
title: Dart / Flutter
description: Generate Dart models and Dio API clients from OpenAPI
---

Generate idiomatic [Dart](https://dart.dev/) model classes and [Dio](https://pub.dev/packages/dio)-based API clients from your OpenAPI specification — ready to drop into any Flutter or Dart project.

## Configuration

Set the `client` option to `dart` and point the `target` at the directory where you want the generated code:

```ts title="orval.config.ts"
import { defineConfig } from 'orval';

export default defineConfig({
myApi: {
input: {
target: './openapi.yaml',
},
output: {
client: 'dart',
target: './my_flutter_package/lib/src/generated/',
},
},
});
```

<Callout type="info">
The `dart` client target is a **directory**, not a file. All generated `.dart` files are written inside it.
</Callout>

## Generated Structure

```
my_flutter_package/lib/src/generated/
├── models/
│ ├── pet.dart
│ ├── create_pet_body.dart
│ ├── error.dart
│ └── models.dart # barrel export
├── api/
│ ├── pets_api.dart
│ ├── users_api.dart
│ └── api.dart # barrel export
└── generated.dart # top-level barrel
```

## Models

Each OpenAPI component schema produces a Dart class with `fromJson`, `toJson`, and `copyWith`:

```dart title="models/pet.dart"
class Pet {
final int id;
final String name;
final String? tag;

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

factory Pet.fromJson(Map<String, dynamic> json) {
return Pet(
id: json['id'] as int,
name: json['name'] as String,
tag: json['tag'] as String?,
);
}

Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'tag': tag,
};
}

Pet copyWith({
int? id,
String? name,
String? tag,
}) {
return Pet(
id: id ?? this.id,
name: name ?? this.name,
tag: tag ?? this.tag,
);
Comment on lines +80 to +89
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching Dart generator/model templates for copyWith nullable fallback patterns..."
rg -n -C3 "copyWith|\\?\\? this\\." --type ts --type js --type md --type dart

echo
echo "Looking specifically for nullable params in copyWith signatures near fallback usage..."
rg -n -C4 "copyWith\\(|\\w+\\?\\s+\\w+|\\?\\? this\\." --type dart --type ts

Repository: orval-labs/orval

Length of output: 2934


Document the copyWith limitation for nullable fields

The example (lines 80-89) correctly reflects what the Dart generator produces, but doesn't explain that the ?? this.field pattern prevents callers from explicitly setting nullable fields to null. Add a note clarifying this limitation, or show an alternative pattern using a sentinel parameter (e.g., Object? tag = _unset) to allow null assignment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/content/docs/guides/dart.mdx` around lines 80 - 89, Add a short note
under the Pet.copyWith example explaining that the current pattern (id: id ??
this.id, name: name ?? this.name, tag: tag ?? this.tag) cannot distinguish a
caller explicitly passing null for a nullable field from passing nothing, so
nullable fields cannot be set to null via this generated copyWith; recommend
either documenting that limitation or showing the alternative sentinel pattern
(e.g., using a sentinel parameter like Object? tag = _unset and checking for the
sentinel inside copyWith) so callers can explicitly assign null to nullable
fields.

}
}
```

## Type Mapping

| OpenAPI | Dart |
|---------|------|
| `string` | `String` |
| `integer` | `int` |
| `number` | `double` |
| `boolean` | `bool` |
| `array` | `List<T>` |
| `object` (no properties) | `Map<String, dynamic>` |
| `string` + `format: date` | `DateTime` (date-only serialization) |
| `string` + `format: date-time` | `DateTime` |
| `string` + `format: binary` | `dynamic` (use `FormData` for uploads) |
| `$ref` | Referenced class |
| `anyOf: [T, null]` | `T?` |
| `anyOf: [T1, T2]` | `dynamic` |

## API Client

Operations are grouped by URL path segment and wrapped in Dio-based API classes:

```dart title="api/pets_api.dart"
import 'package:dio/dio.dart';
import '../models/pet.dart';

class PetsApi {
final Dio _dio;

PetsApi(this._dio);

/// List all pets
Future<Response<dynamic>> listPets({
int? limit,
}) async {
final queryParameters = <String, dynamic>{};
if (limit != null) {
queryParameters['limit'] = limit;
}

final response = await _dio.get<dynamic>(
'/pets',
queryParameters: queryParameters,
);
return response;
}

/// Create a pet
Future<Response<dynamic>> createPet({
required Pet body,
}) async {
final response = await _dio.post<dynamic>(
'/pets',
data: body.toJson(),
);
return response;
}
}
```

### Features

- **Path parameters** use Dart string interpolation: `/pets/$petId`
- **Query parameters** are collected into a map with null checks for optional params
- **Request bodies** are serialized via `toJson()` for `$ref` types
- **Multipart uploads** accept a `FormData` parameter directly
- **Doc comments** are generated from OpenAPI `summary` fields

## Usage in Flutter

Add the generated package to your `pubspec.yaml`:

```yaml title="pubspec.yaml"
dependencies:
dio: ^5.4.0
```

Then use it:

```dart
import 'package:dio/dio.dart';
import 'package:my_flutter_package/my_flutter_package.dart';

final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
final petsApi = PetsApi(dio);

final response = await petsApi.listPets(limit: 10);
```
1 change: 1 addition & 0 deletions docs/content/docs/guides/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"angular",
"solid-start",
"hono",
"dart",
"---Validation & Mocking---",
"zod",
"client-with-zod",
Expand Down
2 changes: 1 addition & 1 deletion docs/content/docs/reference/configuration/output.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default defineConfig({

**Type:** `String | Function`
**Default:** `'axios-functions'`
**Options:** `angular`, `axios`, `axios-functions`, `react-query`, `svelte-query`, `vue-query`, `swr`, `zod`, `hono`, `fetch`, `mcp`
**Options:** `angular`, `axios`, `axios-functions`, `react-query`, `svelte-query`, `vue-query`, `swr`, `zod`, `hono`, `fetch`, `mcp`, `dart`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep the output.client options list fully in sync

Line 28 is still missing valid options (angular-query, solid-start, solid-query), so users won’t see all supported client values.

Suggested fix
-**Options:** `angular`, `axios`, `axios-functions`, `react-query`, `svelte-query`, `vue-query`, `swr`, `zod`, `hono`, `fetch`, `mcp`, `dart`
+**Options:** `angular`, `angular-query`, `axios`, `axios-functions`, `react-query`, `solid-start`, `solid-query`, `svelte-query`, `vue-query`, `swr`, `zod`, `hono`, `fetch`, `mcp`, `dart`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
**Options:** `angular`, `axios`, `axios-functions`, `react-query`, `svelte-query`, `vue-query`, `swr`, `zod`, `hono`, `fetch`, `mcp`, `dart`
**Options:** `angular`, `angular-query`, `axios`, `axios-functions`, `react-query`, `solid-start`, `solid-query`, `svelte-query`, `vue-query`, `swr`, `zod`, `hono`, `fetch`, `mcp`, `dart`
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/content/docs/reference/configuration/output.mdx` at line 28, The docs
list of supported output.client options is missing three valid values; update
the options array shown for output.client (the line containing "`angular`,
`axios`, `axios-functions`, ...") to include `angular-query`, `solid-start`, and
`solid-query` so the list fully reflects all supported client values and stays
in sync with the implementation.


```ts title="orval.config.ts"
export default defineConfig({
Expand Down
12 changes: 12 additions & 0 deletions orval.dart.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export default {
dartApi: {
input: {
target: 'http://localhost:8000/openapi.json',
},
output: {
target: 'lib/generated/',
client: 'dart',
mode: 'single',
},
},
};
1 change: 1 addition & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ export const OutputClient = {
HONO: 'hono',
FETCH: 'fetch',
MCP: 'mcp',
DART: 'dart',
} as const;

export type OutputClient = (typeof OutputClient)[keyof typeof OutputClient];
Expand Down
53 changes: 53 additions & 0 deletions packages/dart/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
{
"name": "@orval/dart",
"version": "8.9.1",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/orval-labs/orval.git",
"directory": "packages/dart"
},
"homepage": "https://orval.dev",
"bugs": {
"url": "https://github.com/orval-labs/orval/issues"
},
"type": "module",
"types": "./dist/index.d.mts",
"exports": {
".": {
"development": "./src/index.ts",
"default": "./dist/index.mjs"
},
"./package.json": "./package.json"
},
"files": [
"dist",
"!dist/**/*.d.ts.map",
"!dist/**/*.d.mts.map"
],
"scripts": {
"build": "tsdown --config-loader unrun",
"dev": "tsdown --config-loader unrun --watch src",
"lint": "eslint .",
"test": "vitest",
"typecheck": "tsc --noEmit",
"clean": "rimraf .turbo dist",
"nuke": "rimraf .turbo dist node_modules"
},
"dependencies": {
"@orval/core": "workspace:*"
},
"devDependencies": {
"eslint": "catalog:",
"rimraf": "catalog:",
"tsdown": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
},
"publishConfig": {
"exports": {
".": "./dist/index.mjs",
"./package.json": "./package.json"
}
}
}
Loading
Loading