Skip to content

How schemas are derived

Every type that appears as a request body, a return type, a path/query/header parameter, a bodyType: in a response annotation or a type: in BodyDoc or QueryDoc is converted into a JSON schema by the builder. Classes are registered once under components.schemas and referenced with $ref.

  • Deciding how to write a model class so its schema comes out right (which fields are required, how nullability shows up).
  • Understanding why a schema is empty ({}) or why a component has a name like Page_User.

If you need a schema the builder cannot derive, register it by hand with OpenApiSpec.putSchema or through OpenApiSpec.extra.

Dart type Schema
String {type: string}
int {type: integer, format: int64}
double {type: number, format: double}
num {type: number}
bool {type: boolean}
DateTime {type: string, format: date-time}
Uri {type: string, format: uri}
dynamic, Object {} (any value)
List<T>, Iterable<T> {type: array, items: <T>}
Set<T> {type: array, items: <T>, uniqueItems: true}
Map<K, V> {type: object, additionalProperties: <V>}
enum E { a, b } {type: string, enum: [a, b]}
enum that declares toJson() {} (left open, because the wire value is up to you)
Any other class outside the Dart SDK (yours or a dependency’s) {$ref: '#/components/schemas/<Name>'}
Anything else (SDK classes not listed) {}

OpenAPI 3.1 expresses null with a type union:

  • int? becomes {type: [integer, null], format: int64}; the same for every primitive, array and map.
  • A nullable enum adds null to both type and enum.
  • A nullable class reference becomes {anyOf: [{$ref: ...}, {type: null}]}.

For a class outside the Dart SDK the builder builds an object schema:

  1. If the unnamed constructor has parameters, each parameter is a property. A parameter is listed in required when it is required (named or positional) and its type is not nullable.
  2. Otherwise each non-static instance field is a property, required when its type is not nullable.

The property names are the Dart parameter or field names, unchanged. If your toJson() uses different keys (for example snake_case), the documented schema will not match the wire format; keep constructor parameter names equal to your JSON keys.

Recursive references work: a class name is reserved before its fields are walked, so a self-referencing field resolves to a $ref.

Each instantiation of a generic class is its own component. Page<User> becomes Page_User and Page<List<Post?>> becomes Page_List_PostNullable. Fields typed T are resolved to the type argument.

A parameter whose type extends gisila’s Form gets a schema built from its FormField members (walking superclasses too):

Field type Schema
StringField {type: string}
EmailField {type: string, format: email}
PasswordField {type: string, format: password}
IntField {type: integer, format: int64}
DoubleField {type: number, format: double}
BoolField {type: boolean}
DateTimeField {type: string, format: date-time}
FileField {type: string, format: binary}
JsonField {type: object}
Other FormField<T> Schema of T

The property key is the Dart field name. Every property is emitted as optional because the builder cannot read required: from the field initializers; Form.bind still enforces it at runtime.

The form body is documented under application/json and application/x-www-form-urlencoded, or only multipart/form-data when the form has a FileField.

class Page<T> {
Page({required this.items, required this.total, this.next});
final List<T> items;
final int total;
final String? next;
}
class User {
User({required this.id, required this.name, this.age});
final String id;
final String name;
final int? age;
}
@Controller('/users')
class UsersController {
@Get('/')
Future<Page<User>> list() async => throw UnimplementedError();
}

Produces two components:

{
"User": {
"type": "object",
"properties": {
"id": {"type": "string"},
"name": {"type": "string"},
"age": {"type": ["integer", "null"], "format": "int64"}
},
"required": ["id", "name"]
},
"Page_User": {
"type": "object",
"properties": {
"items": {"type": "array", "items": {"$ref": "#/components/schemas/User"}},
"total": {"type": "integer", "format": "int64"},
"next": {"type": ["string", "null"]}
},
"required": ["items", "total"]
}
}