scout_openapi
Part of the Scout Application Capability Intelligence Platform
A build_runner plugin that generates OpenAPI 3.0 specifications from annotated
Dart service classes — annotation-driven, zero reflection, works with any Dart or
Flutter project.
Annotate your service methods with @ApiService and @Endpoint, run
dart run build_runner build, and get a valid api_spec.yaml (or .json).
Why
Most Dart/Flutter OpenAPI tooling goes spec → Dart (generating client code from a YAML file). This plugin goes the other direction: Dart → spec. If you maintain the source of truth in Dart service classes rather than a hand-written YAML, this is the tool for you.
Key capabilities that set it apart:
- Separate request/response schemas — when your
fromJsonandtoJsonkey mappings differ (a common pattern in REST APIs), the generator creates distinct request and response schema components automatically. - Field override DSL — surgically change required/optional status or type on a
per-endpoint basis without duplicating model classes. Overrides that reference a
non-existent field fail the build (catching field-rename drift in CI), unless you
set
allow_unresolved_overrides: true. - Generic wrapper / envelope support — generic models like
Page<Pet>,Result<T, E>, orApiResponse<T>are emitted as parameterized schemas (PagePet, …) with type arguments substituted into their fields, instead of collapsing to an opaqueobject. @ApiIgnore— exclude methods or entire classes from the spec without removing them.exclude_internal— keep internal/debug endpoints out of the public spec with one flag, while still generatingx-internal: trueannotations when they are included.- AST-level key extraction — parses
fromJson/toJsonmethod bodies directly; no annotation dependency other than optional@JsonKey.
Quick start
1. Add to dev_dependencies
# pubspec.yaml
dev_dependencies:
scout_openapi: ^0.1.0
build_runner: ^2.4.0
2. Annotate your services
import 'package:scout_openapi/scout_openapi.dart';
@ApiService(basePath: '/pets', tag: 'Pets', tagDescription: 'Manage pets')
class PetService {
@Endpoint(
path: '/',
method: HttpMethod.get,
summary: 'List pets',
queryParams: ['page?:integer', 'limit?:integer(1,100)', 'status?:enum[available,pending,sold]'],
)
Future<Page<Pet>> listPets() async { ... }
@Endpoint(
path: '/{id:integer}',
method: HttpMethod.get,
summary: 'Get a pet by ID',
)
Future<Pet> getPet() async { ... }
@Endpoint(
path: '/',
method: HttpMethod.post,
summary: 'Create a pet',
bodyType: Pet,
body: ['tag?'], // make 'tag' optional in this endpoint's request schema
)
Future<Pet> createPet() async { ... }
@ApiIgnore()
Future<void> _warmCache() async {} // excluded from spec
}
3. Configure build.yaml
targets:
$default:
builders:
scout_openapi:
options:
title: My API
version: 1.0.0
base_url: https://api.example.com
exclude_internal: true
4. Run
dart run build_runner build
Output: api_spec.yaml at your package root.
Annotations reference
@ApiService
Marks a class as a group of API endpoints. All annotated methods become operations under
the service's basePath.
| Field | Type | Default | Description |
|---|---|---|---|
basePath |
String |
'' |
Path prefix for all endpoints in this service |
tag |
String? |
class name | OpenAPI tag for grouping in UIs like Swagger |
tagDescription |
String? |
— | Description shown next to the tag |
@Endpoint
Marks a method as an API endpoint.
| Field | Type | Default | Description |
|---|---|---|---|
path |
String |
required | Path relative to ApiService.basePath. Supports :param and {param:type} syntax |
method |
HttpMethod |
required | get, post, put, patch, delete, head, options |
summary |
String? |
— | Short one-line description |
description |
String? |
— | Longer Markdown description |
operationId |
String? |
auto | Defaults to lowerCamelCase(className)_methodName |
deprecated |
bool |
false |
Marks the operation deprecated |
internal |
bool |
false |
Tags with x-internal: true; excluded when exclude_internal: true |
queryParams |
List<String> |
[] |
Query parameter DSL (see below) |
body |
List<String> |
[] |
Inline body DSL or field overrides for bodyType (see below) |
bodyType |
Type? |
— | Dart class to use as the request body schema |
absolutePath |
bool |
false |
Use path as-is, ignoring basePath |
responses |
Map<String, String> |
{} |
Custom HTTP responses ({'201': 'Created'}), merged over the default 200/400/401/500; a matching key overrides the default description |
@ApiIgnore
Place on a method to exclude it from the spec, or on a class to exclude all its methods.
DSL reference
Path parameters
/pets/{id:integer} → path param 'id' of type integer
/orders/{ref:string} → path param 'ref' of type string
/items/{n} → path param 'n' of type string (default)
The type hint (:integer, :string, etc.) is stripped from the actual path string;
only the clean {id} form appears in the spec.
Query parameters and inline body fields
'field' required string
'field?' optional string
'field:integer' required integer
'field?:boolean' optional boolean
'field:number' required number
'field:array' required array of strings
'field:array[integer]' required array of integers
'field:object' required opaque object
'field:enum[a,b,c]' required string enum
'field:integer(1,100)' required integer, minimum 1, maximum 100
'field:integer(1,)' required integer, minimum 1 only
'field:number(,99.9)' required number, maximum 99.9 only
'a.b' nested: force field 'b' inside object 'a' to required
'a.b?' nested: force field 'b' inside object 'a' to optional
bodyType + body overrides
When both bodyType and body are set, body entries are treated as overrides
to the base schema rather than a replacement. The generator creates a derived schema
component (e.g. PetRequest2) with the specified required/optional/type changes,
leaving the original schema intact.
@Endpoint(
path: '/{id}',
method: HttpMethod.patch,
bodyType: Pet,
body: ['name?', 'status?'], // make both optional for patch
)
Separate request/response schemas
When a model's toJson produces different keys than fromJson expects (e.g. a REST API
that accepts snake_case but responds with camelCase), the generator detects the
mismatch automatically and emits both a Pet (response) component and a PetRequest
(request) component.
This is done by AST-parsing fromJson and toJson method bodies — no annotation
required.
build.yaml options reference
| Option | Default | Description |
|---|---|---|
title |
API Specification |
Spec info.title |
version |
1.0.0 |
Spec info.version |
description |
— | Spec info.description |
base_url |
https://api.example.com |
Written into servers[0].url |
output_format |
yaml |
yaml or json |
output_path |
(derived) | Override output file path |
scan_path |
lib/**/*.dart |
Glob for files containing @ApiService classes |
model_scan_path |
lib/**/*.dart |
Glob for model files (extracts key mappings) |
exclude_internal |
false |
Omit endpoints with internal: true |
allow_unresolved_overrides |
false |
When false, a body override naming a non-existent field fails the build; set true to downgrade to a warning |
contact.name |
— | Spec info.contact.name |
contact.url |
— | Spec info.contact.url |
contact.email |
— | Spec info.contact.email |
Validating the output
After generation, validate the spec with any OpenAPI validator:
# npm
npx @stoplight/spectral-cli lint api_spec.yaml
# or
npx swagger-cli validate api_spec.yaml
How it works
-
Model scan pass — AST-parses every
.dartfile matched bymodel_scan_path, extractingfromJsonandtoJsonkey mappings into a cache keyed bylibraryUri#ClassName. -
Endpoint scan pass — uses
source_gen'sTypeCheckerto find all@ApiService-annotated classes, then all@Endpoint-annotated methods inside them.@ApiIgnoreis checked at both the class and method level. -
Schema building — walks the Dart type graph (including inheritance, generics, enums, nullable types) to build
components/schemas. Uses the cached key mappings to produce correct property names. Detects request/response key divergence and creates separate components when needed. Cycle detection prevents infinite recursion on self-referential models. -
Serialization — emits sorted YAML or JSON. Paths sorted alphabetically, methods sorted by REST convention (
get,post,put,patch,delete, ...).
Part of Scout
scout_openapi is one of the foundation packages of the
Scout Application Capability Intelligence Platform.
Scout runs scout_openapi's endpoint analysis as one of three passes to produce
scout.manifest.json — a machine-readable capability graph consumed by AI systems,
enforced in CI, and maintained at compile time with zero runtime overhead.
License
MIT
Libraries
- scout_openapi
- Scout OpenAPI — generate OpenAPI 3.0 specifications from annotated Dart service classes at compile time.