How schemas are derived
What it is
Section titled “What it is”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.
When to use it
Section titled “When to use it”- 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 likePage_User.
When not to use it
Section titled “When not to use it”If you need a schema the builder cannot derive, register it by hand with
OpenApiSpec.putSchema or through
OpenApiSpec.extra.
How it works
Section titled “How it works”Built-in types
Section titled “Built-in types”| 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) | {} |
Nullability
Section titled “Nullability”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
nullto bothtypeandenum. - A nullable class reference becomes
{anyOf: [{$ref: ...}, {type: null}]}.
Classes
Section titled “Classes”For a class outside the Dart SDK the builder builds an object schema:
- If the unnamed constructor has parameters, each parameter is a property. A parameter is
listed in
requiredwhen it is required (named or positional) and its type is not nullable. - 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.
Generic classes
Section titled “Generic classes”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.
Example
Section titled “Example”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"] }}