JSON Schema Integration
Ack schemas can be automatically converted into JSON Schema objects, allowing you to generate API documentation directly from your validation schemas.
Generating JSON Schemas
Use the toJsonSchema() method available on any AckSchema instance.
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));
}
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 | Any schema |
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() uses stable nullability wrapping rules:
- Primitive/object/list/enum schemas marked with
.nullable()are emitted as:anyOf: [<base-schema>, { "type": "null" }]
Ack.anyOf([...]).nullable()is emitted as nestedanyOf:- outer
anyOffor nullability - inner
anyOffor the union branches
- outer
Ack.discriminated(...).nullable()follows the same nested pattern:- outer
anyOffor nullability - inner
anyOffor discriminated branches
- outer
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": [/* 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.
Limitations:
- Custom Constraints:
Constraint<T>+Validator<T>instances added via.constrain()are not translated to JSON Schema as there's no standard way to represent arbitrary logic. additionalProperties:Ack.object(..., additionalProperties: false)becomesadditionalProperties: false;truebecomesadditionalProperties: {}(open object schema).
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({
'type': Ack.literal('circle'),
'radius': Ack.double().positive(),
}),
'rectangle': Ack.object({
'type': Ack.literal('rectangle'),
'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();