Skip to content

How generation works

gisila_doc ships a source_gen builder. For every class annotated with @Controller it generates an extension named <ClassName>GisilaDoc with one method:

void attachToApp(GisilaApp app, Router router, OpenApiSpec spec, {String prefix = ''});

The method registers each annotated route on router and records the matching OpenAPI operation in spec. Nothing is computed by reflection at runtime; parameter binding code and schema maps are written out as plain Dart at build time.

Read this page when you need to predict what the generator will do with a signature: which parameters become path, query, header or body parameters, which responses are documented, and when a build fails.

For the shape of the JSON schemas themselves, see How schemas are derived.

  1. dart run build_runner build runs the gisila_doc builder on every Dart file in a package that depends on gisila_doc. It writes a .gisila_doc.g.part, which source_gen’s combining builder merges into <file>.g.dart. Your controller file needs part '<file>.g.dart';.
  2. For each method carrying a verb annotation (@Get, @Post, …, or @Route itself), the generator joins the controller path and the method path, converts {name} placeholders to shelf_router’s <name>, and classifies each parameter.
  3. A controller with @Controller but no annotated methods logs a warning and generates nothing.

Each method parameter is classified in this order. The first rule that matches wins.

# Rule Becomes
1 Annotated @Path, @Query, @Header or @Body That location, using the annotation’s name if given
2 Type is shelf Request Passed ctx.request; not documented
3 Type is gisila RequestContext Passed the context; not documented
4 Type extends gisila Form await bindForm(request, T.new); documented as the request body
5 Type extends gisila Service ctx.service<T>(); not documented
6 Name matches a {placeholder} in the full path Required path parameter
7 Type is String, int, double, num, bool or DateTime (nullable or not) Query parameter
8 Anything else, if no body has been bound yet JSON request body
9 Anything else Build error: Could not classify parameter …

Details that follow from the generated code:

  • Path and query values are parsed with gisila’s coerce<T>(). A query parameter is required in the spec (and at runtime) when it is non-nullable and not optional.
  • Dart default values are not applied. A missing optional {int page = 1} is passed to coerce<int> as null, which fails binding with a 400. Declare optional query parameters nullable (int? page) and apply the default in the method body.
  • A header parameter is read as the raw header string, so declare it String?.
  • A JSON body is read with readJsonBody. If the type has a fromJson(Map<String, dynamic>) constructor it is called; List<T> with T.fromJson maps each element; Map<String, ...> is cast. The body is required unless the parameter is optional or nullable.
  • Positional and named parameters are both supported; the generated call passes each the same way it was declared.
Method return type Runtime behaviour Documented success response
Response / Future<Response> Returned unchanged Status only, unless @Returns(bodyType: ...) supplies a schema
void / Future<void> emptyResponse(statusCode: <success>) Status only
Anything else jsonResponse(body: result, statusCode: <success>) application/json with the return type’s schema

The success status is the verb annotation’s statusCode (200, or 201 for @Post, 204 for @Delete), overridden by the first Returns entry when that has a statusCode.

  • 400 with the error envelope when the route binds any input (a form, a body, or a path, query or header parameter).
  • 401 when the route is guarded, and 403 as well when the guards include permissions or roles. See Document authentication.

A response you declare yourself for the same status code replaces the automatic one.

Each route block in attachToApp:

  1. builds the route’s RouteConfig by merging the class @UseRouteConfig, the guards, the annotation’s config: and any @UseMiddleware names;
  2. calls gisilaRoute(app: app, config: ..., routeId: 'METHOD <path>', handler: ...);
  3. registers it with router.<verb>(prefix + path, ...). A route whose path ends in / (such as @Get('/') under /users) is also registered without the slash;
  4. calls spec.putOperation(prefix + openApiPath, verb, Operation(...)).

Argument binding runs inside a try that turns TypeError and FormatException into BadRequestException (a 400). Exceptions thrown by your controller method are not caught there; they reach the app’s error handler.

Schemas and controller tags are registered with spec.putSchema and spec.putTag at the top of attachToApp.

@Controller('/orders', ['Orders'])
class OrdersController {
@Get('/{id}')
Future<Order> getOrder(
String id, // path (matches {id})
OrderService orders, // service, injected
{bool? expand, // query, optional
@Header(name: 'X-Request-Id') String? requestId}, // header
) => orders.get(id, expand: expand ?? false);
@Post('/')
Future<Order> create(CreateOrder body, OrderService orders) =>
orders.create(body); // body: CreateOrder.fromJson(...)
}