JSON Schema Integration
Ack schemas can be automatically converted into JSON Schema objects, allowing you to generate API documentation directly from your validation schemas.
Ack also exposes AckSchemaModel, the canonical export model used by schema
adapter packages. Use schema.toSchemaModel() when you are building a
converter to another target format; JSON Schema is one renderer of that model,
not the source of truth for adapter behavior.
Generating JSON Schemas
Use the toJsonSchema() method available on any AckSchema instance. This is
the same generic Draft-7 renderer used by
schema.toSchemaModel().toJsonSchema().
import 'package:ack/ack.dart';
import 'dart:convert';
// Example User Schema
final userSchema = Ack.object({
'id': Ack.integer().positive().describe('Unique user identifier'),
'name': Ack.string().minLength(2).maxLength(50).describe('User\'s full name'),
'email': Ack.string().email().describe('User\'s email address'),
'role': Ack.enumValues(UserRole.values).withDefault(UserRole.user),
'isActive': Ack.boolean().withDefault(true),
'tags': Ack.list(Ack.string()).unique().describe('List of user tags').nullable(),
'age': Ack.integer().min(0).max(120).nullable().describe('User\'s age'),
}).describe('Represents a user in the system');
void main() {
// Convert the AckSchema to a JSON Schema Object Map
final jsonSchemaMap = userSchema.toJsonSchema();
// Pretty print the JSON representation of the JSON Schema
final jsonEncoder = JsonEncoder.withIndent(' ');
print(jsonEncoder.convert(jsonSchemaMap));
}
Adapter Model
Use toSchemaModel() when you need a reusable, target-independent view of an
Ack schema:
final model = userSchema.toSchemaModel();
final jsonSchemaMap = model.toJsonSchema();
for (final warning in model.warnings) {
print('${warning.code}: ${warning.message}');
}
AckSchemaModel describes the boundary values a schema accepts and exports. It
keeps adapter metadata such as object property ordering and discriminator
metadata. Its JSON Schema renderer emits only generic Draft-7-compatible
output; provider-specific hints belong in adapter renderers.
Output JSON (JSON Schema Object):
{
"type": "object",
"description": "Represents a user in the system",
"properties": {
"id": {
"type": "integer",
"description": "Unique user identifier",
"exclusiveMinimum": 0
},
"name": {
"type": "string",
"description": "User\'s full name",
"minLength": 2,
"maxLength": 50
},
"email": {
"type": "string",
"format": "email",
"description": "User\'s email address"
},
"role": {
"type": "string",
"enum": [
"admin",
"user",
"guest"
],
"default": "user"
},
"isActive": {
"type": "boolean",
"default": true
},
"tags": {
"anyOf": [
{
"type": "array",
"description": "List of user tags",
"items": {
"type": "string"
},
"uniqueItems": true
},
{
"type": "null"
}
]
},
"age": {
"anyOf": [
{
"type": "integer",
"description": "User\'s age",
"minimum": 0,
"maximum": 120
},
{
"type": "null"
}
]
}
},
"required": [
"id",
"name",
"email",
"role",
"isActive",
"tags",
"age"
],
"additionalProperties": false
}
How Constraints Map to JSON Schema
Ack attempts to map its built-in constraints to corresponding JSON Schema keywords:
| Ack Constraint or Schema | JSON Schema Keyword | Notes |
|---|---|---|
minLength(n) | minLength: n | String |
maxLength(n) | maxLength: n | String |
matches(p) | pattern: p | String |
email() | format: email | String |
date() | format: date | String |
datetime() | format: date-time | String |
time() | format: time | String |
uri() | format: uri | String |
uuid() | format: uuid | String |
ipv4() | format: ipv4 | String |
ipv6() | format: ipv6 | String |
enumString([...]) | enum: [...] | String |
min(n) | minimum: n | Number (int/double) |
max(n) | maximum: n | Number (int/double) |
greaterThan(n) | exclusiveMinimum: n | Number (exclusive) |
lessThan(n) | exclusiveMaximum: n | Number (exclusive) |
multipleOf(n) | multipleOf: n | Number (int/double) |
minLength(n) | minItems: n | List (array) |
maxLength(n) | maxItems: n | List (array) |
unique() | uniqueItems: true | List (array) |
nullable() | anyOf: [<schema>, {type: null}] | Any schema |
withDefault(v) | default: v | JSON/export-safe defaults only in AckSchemaModel; unsupported defaults are omitted with a warning |
describe(d) | description: d | Any schema |
Ack.integer() | type: integer | Type |
Ack.double() | type: number | Type |
Ack.string() | type: string | Type |
Ack.boolean() | type: boolean | Type |
Ack.list(...) | type: array, items: {...} | Type |
Ack.object(...) | type: object, properties: {...}, required: [...] | Type |
Shape Stability Notes
toJsonSchema() renders generic Draft-7 JSON Schema and uses stable
nullability rules:
- Primitive/object/list/enum schemas marked with
.nullable()are emitted as:anyOf: [<base-schema>, { "type": "null" }]
Ack.anyOf([...]).nullable()is emitted asanyOf: [{ "anyOf": [...] }, { "type": "null" }]Ack.discriminated(...)is emitted asanyOfwith effective object branches. Each branch contains the exact required discriminatorconst.Ack.discriminated(...).nullable()wraps thatanyOfunion with a second{ "type": "null" }branch.
This means nullable enums are represented as:
{
"anyOf": [
{ "type": "string", "enum": ["admin", "user", "guest"] },
{ "type": "null" }
]
}
And nullable discriminated unions are represented as:
{
"anyOf": [
{ "anyOf": [/* effective discriminated object branches */] },
{ "type": "null" }
]
}
If you build consumers that inspect generated schemas, treat nullability and union composition as separate concerns and avoid assuming enum values always live at the top level.
The nested nullable-union shape is intentional for generic Draft-7 output and
matches Zod v4's toJSONSchema() renderer. Do not flatten it in
AckSchema.toJsonSchema(); provider-specific adapters that require a different
shape should implement that as explicit adapter rendering.
Limitations:
- Custom Constraints:
Constraint<T>+Validator<T>instances added via.constrain()are not translated to JSON Schema because there is no standard way to represent arbitrary logic. additionalProperties:Ack.object(..., additionalProperties: false)becomesadditionalProperties: false;additionalProperties: trueis emitted as the booleantrue.Ack.any(): Runtime validation accepts arbitrary non-null Dart objects. JSON-like adapter exports represent only JSON-compatible values and attach anack_any_json_boundarywarning to theAckSchemaModel.- Date/time range constraints: Draft-7 has no standard
formatMinimumorformatMaximumkeywords. ACK validates.min()and.max()at runtime and records schema-model warnings instead of rendering non-standard keywords. - List item nullability:
Ack.list(...)does not support nullable item schemas yet. Make the list itself nullable withAck.list(item).nullable()when the whole field may be null. - Discriminated branches:
Ack.discriminated(...)owns the discriminator. Branches may omit the discriminator field; compatibleAck.literal(...)orAck.enumString(...)fields are accepted; generated branches expose the exact branch value asconst.
Integrating into API Documentation
You can use the generated JSON Schema map within a larger API documentation structure.
// Assume you have a function to build the full API spec
Map<String, dynamic> buildApiSpecification() {
final userJsonSchema = userSchema.toJsonSchema();
return {
'schemas': {
'User': userJsonSchema
},
'endpoints': {
'/users': {
'post': {
'summary': 'Create a new user',
'requestBody': {
'required': true,
'content': {
'application/json': {
// Reference the generated schema
'schema': {
'\$ref': '#/schemas/User'
}
}
}
}
}
}
}
};
}
// Usage
final fullApiSpec = buildApiSpecification();
print(JsonEncoder.withIndent(' ').convert(fullApiSpec));
This allows you to maintain your validation logic and API documentation source in one place (your Ack schemas).
Schema Descriptions and Metadata
Add descriptions and metadata to your schemas for better documentation:
final userSchema = Ack.object({
'id': Ack.string().uuid().describe('Unique user identifier'),
'name': Ack.string().minLength(1).describe('User\'s full name'),
'email': Ack.string().email().describe('User\'s email address'),
'age': Ack.integer().min(0).max(150).describe('User\'s age in years').optional(),
}).describe('Represents a user in the system');
final jsonSchema = userSchema.toJsonSchema();
// Generated schema will include description fields
Default Values in JSON Schema
Schemas with default values will include them in the generated JSON Schema:
final configSchema = Ack.object({
'theme': Ack.enumValues(Theme.values).withDefault(Theme.light),
'notifications': Ack.boolean().withDefault(true),
'maxItems': Ack.integer().min(1).max(100).withDefault(10),
});
final jsonSchema = configSchema.toJsonSchema();
// Will include "default" properties in the JSON Schema
Complex Schema Patterns
JSON Schema generation works with all Ack schema types:
// Union types
final mixedValueSchema = Ack.anyOf([
Ack.string(),
Ack.integer(),
Ack.boolean(),
]);
// Discriminated unions
final shapeSchema = Ack.discriminated(
discriminatorKey: 'type',
schemas: {
'circle': Ack.object({
'radius': Ack.double().positive(),
}),
'rectangle': Ack.object({
'width': Ack.double().positive(),
'height': Ack.double().positive(),
}),
},
);
// Nested arrays and objects
final complexSchema = Ack.object({
'users': Ack.list(userSchema).minLength(1),
'metadata': Ack.object({
'version': Ack.string(),
'tags': Ack.list(Ack.string()).unique(),
}).optional(),
});
// All generate valid JSON Schema
final mixedJson = mixedValueSchema.toJsonSchema();
final shapeJson = shapeSchema.toJsonSchema();
final complexJson = complexSchema.toJsonSchema();