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 fromJson and toJson key 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>, or ApiResponse<T> are emitted as parameterized schemas (PagePet, …) with type arguments substituted into their fields, instead of collapsing to an opaque object.
  • @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 generating x-internal: true annotations when they are included.
  • AST-level key extraction — parses fromJson/toJson method 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

  1. Model scan pass — AST-parses every .dart file matched by model_scan_path, extracting fromJson and toJson key mappings into a cache keyed by libraryUri#ClassName.

  2. Endpoint scan pass — uses source_gen's TypeChecker to find all @ApiService-annotated classes, then all @Endpoint-annotated methods inside them. @ApiIgnore is checked at both the class and method level.

  3. 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.

  4. 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.