Describe request and response models
This guide shows the three ways to get request and response schemas into the document: typed
parameters and return types (inferred), forms, and explicit annotations for handlers that work
with raw Request/Response. At the end your operations show complete schemas for bodies,
success responses and errors.
Prerequisites
Section titled “Prerequisites”- A working gisila_doc controller (see Document an API).
- Know the schema rules, especially that property names are taken from constructor parameter names.
1. Use typed bodies and return types
Section titled “1. Use typed bodies and return types”A non-primitive parameter that is not a Form, Service, Request or RequestContext is the
JSON body. Give its class a fromJson(Map<String, dynamic>) constructor so the generated code
can build it.
import 'package:gisila_doc/gisila_doc.dart';
part 'products_controller.g.dart';
class CreateProduct { CreateProduct({required this.name, required this.priceCents, this.tags = const []});
factory CreateProduct.fromJson(Map<String, dynamic> json) => CreateProduct( name: json['name'] as String, priceCents: json['priceCents'] as int, tags: (json['tags'] as List?)?.cast<String>() ?? const [], );
final String name; final int priceCents; final List<String> tags;}
class Product { Product({required this.id, required this.name, required this.priceCents}); final String id; final String name; final int priceCents;
Map<String, dynamic> toJson() => {'id': id, 'name': name, 'priceCents': priceCents};}
@Controller('/products', ['Products'])class ProductsController { @Post('/') Future<Product> create(CreateProduct body, ProductService products) => products.create(body);}The document gets CreateProduct (with name and priceCents required; tags has a default,
so it is optional) and Product components. A payload that fails fromJson with a TypeError
or FormatException is answered with 400.
Keep the fromJson/toJson keys equal to the constructor parameter names. The schema uses the
Dart names, so toJson() emitting price_cents would make the document wrong.
2. Use a form for validated input
Section titled “2. Use a form for validated input”A Form subclass parameter is bound with bindForm and validated by its fields. It is
documented as application/json and application/x-www-form-urlencoded, or as
multipart/form-data when it has a FileField.
class AvatarForm extends Form { final caption = StringField(name: 'caption'); final image = FileField(name: 'image', required: true);
@override List<FormField<Object?>> collectFields() => [caption, image];}
@Put('/{id}/avatar', summary: 'Upload an avatar')Future<void> avatar(String id, AvatarForm form, UserService users) => users.saveAvatar(id, form);Form properties are all documented as optional; the runtime still enforces required: true.
3. Wrap collections in generic envelopes
Section titled “3. Wrap collections in generic envelopes”Each instantiation of a generic class is its own component, so a Page<T> wrapper documents
correctly for every T:
class Page<T> { Page({required this.items, required this.total}); final List<T> items; final int total;
Map<String, dynamic> toJson() => {'items': items, 'total': total};}
@Get('/')Future<Page<Product>> list(ProductService products, {int? page}) => products.page(page ?? 1);This produces a Page_Product component whose items reference Product.
4. Document error responses
Section titled “4. Document error responses”Add each error status with @ApiResponse. Give it a bodyType when the body has a fixed
shape.
class ConflictError { ConflictError({required this.message, required this.existingId}); final String message; final String existingId;}
@Post('/')@ApiResponse(409, description: 'SKU already exists', bodyType: ConflictError)Future<Product> create(CreateProduct body, ProductService products) => products.create(body);400 (for routes that bind input), 401 and 403 (for guarded routes) are added for you with
gisila’s error envelope. Declaring the same code with @ApiResponse replaces the automatic one.
5. Document raw Request/Response handlers
Section titled “5. Document raw Request/Response handlers”When a handler parses the request itself and returns a Response, the generator cannot see the
shapes. Describe them:
@Post( '/import', summary: 'Bulk import products', body: BodyDoc(ImportRequest, description: 'Products to import'), responses: [ Returns(statusCode: 202, bodyType: ImportJob, description: 'Import queued'), Returns(statusCode: 413, description: 'Too many products'), ],)@QueryDoc('dryRun', type: bool, description: 'Validate without saving')Future<Response> importProducts(Request request) async { final dryRun = request.url.queryParameters['dryRun'] == 'true'; final json = await readJsonBody(request) as Map<String, dynamic>; final job = await importer.start(json, dryRun: dryRun); return jsonResponse(body: job, statusCode: 202);}6. Register schemas the generator cannot derive
Section titled “6. Register schemas the generator cannot derive”spec.putSchema('GeoPoint', { 'type': 'object', 'properties': { 'lat': {'type': 'number'}, 'lng': {'type': 'number'}, }, 'required': ['lat', 'lng'],});putSchema keeps the first registration of a name, so call it before attachToApp if you want
to replace a generated schema with your own.
Verify it works
Section titled “Verify it works”dart run build_runner builddart run bin/server.dart &curl -s localhost:8080/openapi.json | jq '.components.schemas | keys'# ["ConflictError", "CreateProduct", "ImportJob", "ImportRequest", "Page_Product", "Product"]In Swagger UI, expand an operation and switch the response to Schema to check required fields and nullability.
Next steps
Section titled “Next steps”- How schemas are derived for every type rule.
- Response annotations reference.
- Generate API clients.