---
title: JSON Schema Integration
---

Your Ack [schema](../core-concepts/schemas.mdx) already describes your data's shape — so you can export it as JSON Schema (Draft-7) and reuse it to document an API, drive a form library, or define an LLM tool, all from one source of truth.

## Importing published schemas

Use strict `Ack.fromJsonSchema()` to turn a decoded JSON Schema document
(a map or boolean, not a JSON string) into an
`AckSchema<Object, Object>`. The input dialect is draft 2020-12, including
when `$schema` is absent. Other explicitly declared dialects are rejected.

```dart
import 'package:ack/ack.dart';

final AckSchema<Object, Object> schema = Ack.fromJsonSchema({
  'type': 'object',
  'properties': {
    'name': {'type': 'string'},
    'age': {'type': 'integer', 'minimum': 0},
  },
  'required': ['name'],
  'additionalProperties': false,
});

final user = schema.parse({'name': 'Ada', 'age': 36});
final exported = schema.toJsonSchema();
```

The returned abstract `AckSchema<Object, Object>` composes with native
Ack schemas; its runtime implementation is internal. It validates JSON values
without injecting defaults or applying coercions. Parsing returns a detached,
recursively unmodifiable value.
Required means **key present**, not **non-null**; an omitted `type` does not
implicitly require an object, string, or array. Integral numbers such as `1.0`
match `integer`, and null array elements are supported.

### Design: reuse the existing export boundary

`AckSchema.toJsonSchema()` remains the export API. Imported schemas participate
in the same `AckSchema` → `AckSchemaModel` → JSON Schema path as native schemas;
the builder bridge delegates to that path too. There is no second public
exporter or dependency on a Flutter runtime.

You can import an existing export directly:

```dart
final document = Ack.object({'name': Ack.string()}).toJsonSchema();
final AckSchema<Object, Object> schema = Ack.fromJsonSchema(document);
final exportedAgain = schema.toJsonSchema();
```

The source document and executable validator are distinct.
`Ack.fromJsonSchema()` is the single import entry point and always imports
strictly. The importer preserves supported JSON validation semantics,
not the original Dart declaration: JSON cannot recover codecs, Dart model
types, custom callbacks, or refinements that were absent from the export.
Exports containing unsupported keywords are rejected.

### Supported import subset

| Area | Keywords |
| --- | --- |
| Schemas and types | Boolean schemas, `type` (including lists and `null`) |
| Equality | `enum`, `const`, including nested JSON values and empty enums |
| Objects | `properties`, `required`, boolean/schema `additionalProperties`, `minProperties`, `maxProperties` |
| Arrays | Single-schema `items`, `minItems`, `maxItems`, `uniqueItems` |
| Strings | `minLength`, `maxLength` (Unicode code points) |
| Numbers | `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum` |
| Composition | `anyOf`, `allOf`, exclusive `oneOf`, `not`; sibling assertions are intersected |
| References | `$ref`, `$id`, `$anchor`, `$defs`, `definitions`; local/cross-document JSON Pointers and structural recursion |
| Annotations | `title`, `description`, `default`, `examples`, `$comment`, `readOnly`, `writeOnly`, `deprecated` |

This is deliberately not a complete JSON Schema validator. Unsupported
keywords include `format`, `pattern`, `multipleOf`, `patternProperties`,
`prefixItems`, `contains`, conditional/dependent/unevaluated keywords, dynamic
references, and custom vocabulary declarations. Unknown extension keywords are
also reported. `format` is not enforced, even for formats Ack's native string
schemas can validate. References to the 2020-12 meta-schema fail strict import.
The meta-schema itself is not validated.

### References and bundles

Supply decoded documents by retrieval URI. Relative keys and references resolve
against `baseUri`; nested `$id` values establish new resource scopes. When no
base is provided, the importer uses `ack-import:///root.json`. The importer
never reads files or fetches URLs.

```dart
final AckSchema<Object, Object> schema = Ack.fromJsonSchema(
  {r'$ref': r'types.json#/$defs/message'},
  baseUri: Uri.parse('https://example.com/protocol/root.json'),
  documents: {
    Uri.parse('types.json'): {
      r'$defs': {
        'message': {
          'type': 'object',
          'properties': {'text': {'type': 'string'}},
          'required': ['text'],
        },
      },
    },
  },
);
```

Exports are self-contained, Draft-7-compatible documents with root-scoped
`definitions` and rewritten references. Original IDs, reference spellings,
and document layout are not preserved. Validation equivalence is the contract,
not textual identity. Imported schemas can be nested inside native objects and
combined with other imported or lazy schemas without losing reference scope.
Explicit JSON Pointers may also target schemas inside extension containers,
such as A2UI's `#/components/Text`. Such containers are not scanned as schemas
unless referenced. Empty and duplicate enums are normalized to valid Draft-7
representations without changing the values they accept.

### Import failures and diagnostics

Unsupported features throw
`JsonSchemaImportException`, whose `diagnostics` identify each source document,
keyword, JSON Pointer, reason code, and explanation. Assertion diagnostics
cover the validation graph reachable from the imported root. Unsupported
assertions in unused definitions or unrelated supplied documents do not make
the import fail. Schema resource IDs still need to be unambiguous.

Malformed supported keywords, unresolved references, duplicate resource IDs,
unsupported dialects, and reference cycles that do not descend into an instance
are errors. The importer checks schema structure and the keywords it compiles;
it is not full meta-schema validation of unused definitions or unsupported
vocabularies. Structural recursion is supported without adding an arbitrary
validation-depth constraint.

### Importing json_schema_builder models

The existing bridge works in both directions:

```dart
import 'package:ack/ack.dart';
import 'package:ack_json_schema_builder/ack_json_schema_builder.dart';
import 'package:json_schema_builder/json_schema_builder.dart' as jsb;

final AckSchema<Object, Object> ackSchema =
    jsb.Schema.string(minLength: 2).toAckSchema();
final builderSchema = ackSchema.toJsonSchemaBuilder();
```

`toAckSchema({baseUri, documents})` is strict and returns the validator directly.
It accepts `Map<Uri, jsb.Schema>` bundles.

MCP registration compatibility is deferred to separate work; importing a
schema does not establish compatibility with a tool-registration API.

### A2UI compatibility and verification

Offline regression fixtures pin five representative A2UI v0.9 protocol schemas
to upstream commit `f5e945a93da36a5d126228f2ad6fbe68809ef8f7`.

The basic catalog is **not an exact supported import**: it additionally uses
unevaluated-property checks, conditionals, patterns, formats, and an extension
keyword. Import therefore fails with diagnostics. The client event schema
reports its date-time format, and client capabilities report the three
meta-schema references. Test the specific catalog your application uses;
successful validation of examples does not prove complete catalog conformance.

The committed conformance tests contain 600 upstream JSON Schema instance cases
across 178 groups in the supported subset, including supplied remote documents.
They check parse, encode, and re-imported exports without network access.
Unsupported cases are not silently skipped within those fixtures.

## Generating JSON schemas

Call `toJsonSchema()` on any `AckSchema` instance. This is the same generic Draft-7 renderer used by `schema.toSchemaModel().toJsonSchema()`.

```dart
import 'dart:convert';

import 'package:ack/ack.dart';

enum UserRole { admin, user, guest }

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));
}
```

> **Building an adapter package?** Most users only need `toJsonSchema()`
> (above). To convert Ack schemas to another target format, render from
> `schema.toSchemaModel()` (the canonical `AckSchemaModel`) rather than the JSON
> Schema map. Start with the [adapter quickstart](./schema-converter-quickstart.mdx),
> then use the [complete adapter authoring guide](./creating-schema-converter-packages.mdx)
> for architecture and testing guidance.

## Adapter model

Use `toSchemaModel()` for a reusable, target-independent view of an Ack schema:

```dart
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, keeping adapter metadata such as property ordering and discriminator info. Its JSON Schema renderer emits only generic Draft-7-compatible output; provider-specific hints belong in adapter renderers.

**Output JSON (JSON Schema Object):**

```json
{
  "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 maps its [built-in constraints](../core-concepts/validation.mdx) to corresponding JSON Schema keywords:

| Ack Constraint or Schema | JSON Schema Keyword | Notes |
| :----------------------- | :------------------ | :---- |
| [`minLength(n)`](../core-concepts/validation.mdx#string-constraints) | `minLength: n` | String |
| [`maxLength(n)`](../core-concepts/validation.mdx#string-constraints) | `maxLength: n` | String |
| [`matches(p)`](../core-concepts/validation.mdx#string-constraints) | `pattern: p` | String |
| [`email()`](../core-concepts/validation.mdx#string-constraints) | `format: email` | String |
| [`date()`](../core-concepts/validation.mdx#string-constraints) | `format: date` | String |
| [`datetime()`](../core-concepts/validation.mdx#string-constraints) | `format: date-time` | String |
| [`time()`](../core-concepts/validation.mdx#string-constraints) | `format: time` | String |
| [`uri()`](../core-concepts/validation.mdx#string-constraints) | `format: uri` | String |
| [`uuid()`](../core-concepts/validation.mdx#string-constraints) | `format: uuid` | String |
| [`ipv4()`](../core-concepts/validation.mdx#string-constraints) | `format: ipv4` | String |
| [`ipv6()`](../core-concepts/validation.mdx#string-constraints) | `format: ipv6` | String |
| [`Ack.enumString([...])`](../core-concepts/validation.mdx#string-constraints) | `enum: [...]` | String |
| [`min(n)`](../core-concepts/validation.mdx#number-constraints) | `minimum: n` | Number |
| [`max(n)`](../core-concepts/validation.mdx#number-constraints) | `maximum: n` | Number |
| [`greaterThan(n)`](../core-concepts/validation.mdx#number-constraints) | `exclusiveMinimum: n` | Number (exclusive) |
| [`lessThan(n)`](../core-concepts/validation.mdx#number-constraints) | `exclusiveMaximum: n` | Number (exclusive) |
| [`multipleOf(n)`](../core-concepts/validation.mdx#number-constraints) | `multipleOf: n` | Number |
| [`minLength(n)`](../core-concepts/validation.mdx#list-constraints) | `minItems: n` | List (array) |
| [`maxLength(n)`](../core-concepts/validation.mdx#list-constraints) | `maxItems: n` | List (array) |
| [`unique()`](../core-concepts/validation.mdx#list-constraints) | `uniqueItems: true` | List (array) |
| [`nullable()`](../core-concepts/schemas.mdx#optional-vs-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()`](../core-concepts/schemas.mdx#number) | `type: integer` | Type |
| [`Ack.double()`](../core-concepts/schemas.mdx#number) | `type: number` | Type |
| [`Ack.string()`](../core-concepts/schemas.mdx#string) | `type: string` | Type |
| [`Ack.boolean()`](../core-concepts/schemas.mdx#boolean) | `type: boolean` | Type |
| [`Ack.list(...)`](../core-concepts/schemas.mdx#list) | `type: array`, `items: {...}` | Type |
| [`Ack.object(...)`](../core-concepts/schemas.mdx#object) | `type: object`, `properties: {...}`, `required: [...]` | Type |
| [`Ack.lazy(...)`](../core-concepts/schemas.mdx#recursive-schemas) | `definitions`, `$ref` | Recursive type |

## Shape stability notes

`toJsonSchema()` renders generic Draft-7 JSON Schema with stable nullability rules:

- Primitive/object/list/enum schemas marked with `.nullable()` are emitted as:
  - `anyOf: [<base-schema>, { "type": "null" }]`
- `Ack.anyOf([...]).nullable()` is emitted as
  `anyOf: [{ "anyOf": [...] }, { "type": "null" }]`
- `Ack.discriminated(...)` is emitted as `anyOf` with effective object
  branches. Each branch contains the exact required discriminator `const`.
- `Ack.discriminated(...).nullable()` wraps that `anyOf` union with a second
  `{ "type": "null" }` branch.
- `Ack.lazy(name, ...)` is emitted as a Draft-7 `definitions` entry and local
  `$ref` values such as `{ "$ref": "#/definitions/Category" }`.
- Non-null lazy refs that carry metadata such as `description` use `allOf`
  around the `$ref` so Draft-7 validators do not ignore that metadata as a
  `$ref` sibling. Nullable lazy refs keep metadata beside the top-level `anyOf`.

This means nullable enums are represented as:

```json
{
  "anyOf": [
    { "type": "string", "enum": ["admin", "user", "guest"] },
    { "type": "null" }
  ]
}
```

And nullable discriminated unions are represented as:

```json
{
  "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 don't assume 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. Don't flatten it in `AckSchema.toJsonSchema()`; provider-specific adapters that need a different shape should implement explicit adapter rendering.

**Limitations:**

-   **Custom Constraints:** [`Constraint<T>` + `Validator<T>`](./custom-validation.mdx)
    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)`
    becomes `additionalProperties: false`; `additionalProperties: true` is
    emitted as the boolean `true`.
-   **`Ack.map(valueSchema)`:** Exported as
    `{"type": "object", "additionalProperties": <value schema>}`. Map keys are
    always strings, so no `propertyNames` keyword is emitted.
-   **`Ack.any()`:** Runtime validation accepts non-null JSON-safe values.
    JSON-like adapter exports represent those JSON-compatible values and attach
    an `ack_any_json_boundary` warning to the `AckSchemaModel`.
-   **`Ack.lazy()` runtime checks:** Recursive structure is exported with
    Draft-7 `definitions` / `$ref`. Constraints and refinements added directly
    to the lazy reference are runtime-only and are reported as schema-model
    warnings rather than emitted beside `$ref`.
-   **Date/time range constraints:** Draft-7 has no standard `formatMinimum` or
    `formatMaximum` keywords. 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 with `Ack.list(item).nullable()`
    when the whole field may be null.
-   **Discriminated branches:** `Ack.discriminated(...)` owns the discriminator.
    Branches may omit the discriminator field; compatible `Ack.literal(...)` or
    `Ack.enumString(...)` fields are accepted; generated branches expose the
    exact branch value as `const`.

## Integrating into API documentation

Use the generated JSON Schema map within a larger API documentation structure.

```dart
// 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 keeps your validation logic and API documentation in one place.

## Advanced JSON Schema features

### Schema descriptions and metadata

Add descriptions and metadata to your schemas for better documentation:

```dart
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();
// Output includes description fields
```

### Default values in JSON Schema

Schemas with default values include them in the generated JSON Schema:

```dart
enum Theme { light, dark }

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();
// Output includes "default" properties
```

### Complex schema patterns

JSON Schema generation works with all Ack schema types:

```dart
// 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 produce valid JSON Schema
final mixedJson = mixedValueSchema.toJsonSchema();
final shapeJson = shapeSchema.toJsonSchema();
final complexJson = complexSchema.toJsonSchema();
```
