-
-
Notifications
You must be signed in to change notification settings - Fork 624
Feat: Dart client #3315
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Feat: Dart client #3315
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| 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, | ||
| ); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## 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); | ||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,7 @@ | |
| "angular", | ||
| "solid-start", | ||
| "hono", | ||
| "dart", | ||
| "---Validation & Mocking---", | ||
| "zod", | ||
| "client-with-zod", | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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` | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Keep the Line 28 is still missing valid options ( 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
Suggested change
🤖 Prompt for AI Agents |
||||||
|
|
||||||
| ```ts title="orval.config.ts" | ||||||
| export default defineConfig({ | ||||||
|
|
||||||
| 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', | ||
| }, | ||
| }, | ||
| }; |
| 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" | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
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.fieldpattern prevents callers from explicitly setting nullable fields tonull. Add a note clarifying this limitation, or show an alternative pattern using a sentinel parameter (e.g.,Object? tag = _unset) to allownullassignment.🤖 Prompt for AI Agents