Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ temp/

# AI Agent workspace
.augment/
/.agents/plans/

# dart_apitool generated files
api-model-*.json
Expand Down
80 changes: 44 additions & 36 deletions docs/guides/json-schema-integration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -107,34 +107,19 @@ treat `format` as an annotation unless configured to assert it.
| Strings | `minLength`, `maxLength` (Unicode code points), `pattern` (unanchored ECMA-262 search in Unicode mode) |
| Numbers | `minimum`, `maximum`, `exclusiveMinimum`, `exclusiveMaximum`, `multipleOf` (exact decimal division) |
| Composition | `anyOf`, `allOf`, exclusive `oneOf`, `not`, `if`/`then`/`else`; 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`, `x-*` vendor extensions |
| Format | `date-time`, `date`, `email`, `uuid`, `uri`, `ipv4`, `ipv6` (strings); `int32`, `int64`, `uint32`, `uint64`, `float`, `double` (numbers) |

`pattern` compiles with ECMA-262's `u` flag, as the JSON Schema Test Suite
requires and Ajv does by default. `.` matches one code point, so
`pattern: "^.$"` accepts `"😀"`, and `\p{...}` is a Unicode property escape.
A pattern that is invalid in Unicode mode fails import with an
`invalid_schema` diagnostic. For example, an escaped hyphen outside a character
class (`^\d{3}\-\d{4}$`) is invalid; write the hyphen unescaped.

`Ack.string().matches()` uses Dart's default `RegExp`, without `u`. A native
pattern can match differently after export and import, and a native pattern
that is invalid in Unicode mode exports a schema that this importer and Ajv
reject.

Both native `.multipleOf()` and imported `multipleOf` divide exactly on the
decimal digits of the number, as the JSON Schema Test Suite requires, so `0.3`
and `0.6` are multiples of `0.1`, `1e308` is a multiple of `0.5`, and an
unrounded binary-float sum such as `0.1 + 0.2` (`0.30000000000000004`) is not.

This is deliberately not a complete JSON Schema validator. Unsupported
keywords include `patternProperties`, `prefixItems`,
`minContains`, `maxContains`, dependent/unevaluated keywords, dynamic
references, and custom vocabulary declarations. Non-`x-*` unknown extension
keywords and unsupported format labels are also reported. References to the
2020-12 meta-schema fail strict import.
The meta-schema itself is not validated.
| References | `$ref`, `$dynamicRef`, `$id`, `$anchor`, `$dynamicAnchor`, `$defs`, `definitions`; local/cross-document pointers and structural recursion |
| Annotations | `title`, `description`, `default`, `examples`, `$comment`, `readOnly`, `writeOnly`, `deprecated`, `format`, content keywords, unknown extensions |

`pattern` uses Unicode-mode matching, including Unicode property escapes.
Imported schemas do not coerce instance values or inject `default` values.

The complete required draft 2020-12 test suite passes for source validation
and export/re-import. Optional vocabulary coverage remains partial; the
repository capability matrix tracks the per-vocabulary limits.
`format` is an annotation by default. Pass `assertFormats: true` to enforce
supported formats, or supply a dialect requiring the format-assertion
vocabulary. Unknown asserted formats fail compilation. Standard 2020-12
meta-schema resources resolve offline.

### References and bundles

Expand All @@ -161,15 +146,16 @@ final AckSchema<Object, Object> schema = Ack.fromJsonSchema(
);
```

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,
Exports preserve imported Draft 2020-12 resources by default (`toJsonSchema()`
and `toSchemaModel()`). To lower compatible imported schemas into Draft-7
`definitions` and rewritten references, call `toJsonSchemaDraft7()` or
`toSchemaModelDraft7()`; imported schemas that require 2020-12-only semantics
reject Draft-7 lowering explicitly. 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.
unless referenced.

### Import failures and diagnostics

Expand Down Expand Up @@ -240,9 +226,9 @@ final AckSchema<Object, Object> ackSchema =
final builderSchema = ackSchema.toJsonSchemaBuilder();
```

`toAckSchema({baseUri, documents})` is strict and returns the validator directly.
It accepts `Map<Uri, jsb.Schema>` bundles and asserts supported `date-time`
formats through Ack's importer.
`toAckSchema({baseUri, documents, assertFormats})` returns the validator
directly. It accepts `Map<Uri, jsb.Schema>` bundles. Formats are annotations
unless `assertFormats: true` or a supplied dialect enables assertions.

MCP registration compatibility is deferred to separate work; importing a
schema does not establish compatibility with a tool-registration API.
Expand All @@ -266,6 +252,28 @@ date-time fixture adds 43 cases. The supported-subset tests check parse,
encode, and re-imported exports without network access. Unsupported cases are
not silently skipped within that fixture.

## Generating Dart models from draft 2020-12

Enable the opt-in `ack_generator:ack_json_schema` builder for a
`*.schema.json` source. In `model_mode: auto` it retains typed `@AckInfer`
fields and `copyWith` for the representable v1 subset. A valid schema with
other 2020-12 behavior generates validated value models instead; their
`value` holds immutable JSON and their `parse`, `safeParse`, `encode`,
`safeEncode`, and `toJson` methods use the source validator. Set
`model_mode: validated` to force this exact path even for simple shapes,
including the typed `int` and UTF-16 string-length edge cases.

The builder also accepts `assert_formats: true` and an offline `documents`
mapping from retrieval URI to package-local JSON asset path. It never fetches
URLs. Source `$defs` generate named models, and a validating document root
generates `Root`; arbitrary definition keys get stable Dart class names.
Unknown required vocabularies, unsupported asserted formats, malformed
schemas, and unresolved resources fail the build with JSON-pointer diagnostics.
See the [generator package README](https://pub.dev/packages/ack_generator)
for `build.yaml` syntax. The required 2020-12 corpus generates offline with
its supplied resources; generated runtime parity is also exercised in a real
`build_runner` consumer for representative advanced features.

## Generating JSON schemas

Call `toJsonSchema()` on any `AckSchema` instance. This is the same generic Draft-7 renderer used by `schema.toSchemaModel().toJsonSchema()`.
Expand Down
55 changes: 55 additions & 0 deletions docs/json-schema-2020-12-capabilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Draft 2020-12 capability matrix

This matrix tracks four different claims. **Compile** means `Ack.fromJsonSchema`
accepts a schema and resolves its declarations. **Validate** means its instance
semantics are exercised. **Export** means `toJsonSchema()` preserves the
meaning under draft 2020-12. **Generate** means the opt-in
`ack_generator:ack_json_schema` builder emits a usable typed or validated Dart
model. `model_mode: auto` keeps typed v1 output where possible and uses a
validated value-model fallback otherwise; `model_mode: validated` forces the
source validator for exact parse/encode behavior. A keyword is not fully
supported merely because a compiler accepts it.

Status: **yes** = implemented and covered, **partial** = known gaps, **no** =
unsupported or not yet implemented. `toJsonSchema()` and `toSchemaModel()`
preserve imported Draft 2020-12 resources by default (`toJsonSchemaDraft7()`
and `toSchemaModelDraft7()` lower compatible imports to Draft-7); native Ack
schemas remain Draft-7.

| Vocabulary / keywords | Compile | Validate | Export | Generate | Remaining work |
|---|---|---|---|---|---|
| Core: boolean schemas, `$schema`, `$id`, `$defs`, `$ref`, pointers, `$anchor`, supplied documents | partial | yes | yes | yes | All 384 required groups generate offline; builder `documents` maps retrieval URIs to local assets. Custom dialects and vocabularies remain bounded by the importer. |
| Core: `$dynamicAnchor`, `$dynamicRef` | yes | yes | yes | yes | Validated value models retain dynamic scope; typed field inference remains unavailable. |
| Core: `$vocabulary` | partial | partial | partial | partial | Supplied custom dialects select supported vocabularies; unknown required vocabularies fail and unknown optional vocabularies remain inert. |
| Core: unknown keywords | yes | n/a | partial | yes | Value models preserve inert annotations; unknown required vocabularies fail. |
| Applicators: `properties`, `additionalProperties`, `propertyNames`, `items`, `allOf`, `anyOf`, `oneOf`, `not` | yes | yes | yes | yes | Typed output covers simple forms; general compositions use validated value models. |
| Applicators: `contains`, `if`, `then`, `else` | yes | yes | yes | yes | Validated value models retain the source assertions; typed fields remain a follow-up. |
| Applicators: `patternProperties`, `dependentSchemas`, `prefixItems` | yes | yes | yes | yes | Validated value models retain the source assertions; typed fields remain a follow-up. |
| Unevaluated: `unevaluatedProperties`, `unevaluatedItems` | yes | yes | yes | yes | Validated value models retain evaluated-location semantics; typed fields remain a follow-up. |
| Validation: `type`, `enum`, `const`, `required`, length/number/array/object bounds, `pattern`, `uniqueItems` | yes | yes | yes | yes | General JSON enum/const use value models. Typed `int` and UTF-16 string-length edges require `model_mode: validated` for exact parity. |
| Validation: `multipleOf`, `minContains`, `maxContains`, `dependentRequired` | yes | yes | yes | yes | Validated value models bind the compiled source schema. |
| Metadata: `title`, `description`, `default`, `examples`, `deprecated`, `readOnly`, `writeOnly`, `$comment` | yes | n/a | yes | yes | Value models retain annotations without injecting defaults or access projections. |
| Format annotation / assertion | yes | yes | yes | yes | `assert_formats: true` enables supported assertions; unknown asserted formats fail compilation by Ack's contract. |
| Content: `contentEncoding`, `contentMediaType`, `contentSchema` | yes | n/a | yes | yes | Preserved as annotations without implicit decoding. |

## Verification contract

- Run the **complete pinned upstream** draft 2020-12 suite and remote fixtures
offline, organized by vocabulary. Do not silently skip unsupported groups.
- Run format cases in annotation and assertion modes separately. Include
Unicode property escapes and numeric/collection equality cases.
- Generated parse and encode must agree with the compiled source validator,
including property presence and nullable roots/elements. Wrapper fallback is
explicit, documented and schema-validated.

The complete **required** suite passes 384 groups in source-validation and
export/re-import modes. The optional format-assertion corpus passes 30 groups
in both modes, with its unknown-format group checked against Ack's explicit
fail-on-unknown-asserted-format contract. All 384 required groups also generate
source offline with their supplied resources. A real `build_runner` consumer
verifies generated parse/encode for
representative recursive, dynamic-reference, composition, unevaluated, numeric,
format-assertion, and external-resource cases. This is not a separate runtime
execution of every generated official case; other optional vocabularies remain
partially covered. The historical selected fixture remains a round-trip
regression corpus.
10 changes: 9 additions & 1 deletion llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ There is no `@AckSchema()` annotation; `AckSchema` is the runtime schema type.

1. `ack`: core runtime validation, codecs, generated-model support, and the
code-generation annotations
2. `ack_generator`: generates models from schemas and schemas from models
2. `ack_generator`: generates models from fluent Ack or draft 2020-12 JSON
Schema bundles, and schemas from classes
3. `ack_firebase_ai`: converts Ack schemas to Firebase AI structured-output schemas
4. `ack_json_schema_builder`: converts Ack schemas to `json_schema_builder` schemas
5. `ack_mcp_dart`: advertises MCP tool input schemas and validates typed arguments with `mcp_dart`
Expand Down Expand Up @@ -59,6 +60,13 @@ Both directions use `@Schemable()`, the same generated parts, and the same
build command. On a top-level schema it is schema-first; on a class it is
class-first. `name` applies only to a schema; every other option applies only
to a class, and generation rejects an option set for the other target.
The opt-in `ack_generator:ack_json_schema` builder translates `*.schema.json`
to typed `@Schemable` models where possible and validated 2020-12 value models
otherwise. Set `model_mode: validated` for exact importer-backed parse/encode;
`assert_formats` and `documents` configure format assertions and offline
external resources. Invalid schemas fail with JSON-pointer diagnostics. Use
`unknown_properties: preserve` only when intentionally widening strict objects
for forward-compatible clients.

Every annotated library declares both generated parts:

Expand Down
3 changes: 2 additions & 1 deletion packages/ack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ Exports preserve supported validation behavior, not textual document identity.
Full A2UI coverage is not supported. MCP registration compatibility remains
separate. See the
[JSON Schema guide](https://concepta.dev/documentation/ack/guides/json-schema-integration)
for the supported subset, recursive bundles, and round-trip guarantees.
and [capability matrix](../../docs/json-schema-2020-12-capabilities.md) for
coverage and round-trip guarantees.

## Documentation

Expand Down
15 changes: 15 additions & 0 deletions packages/ack_generator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
## Unreleased

### Added

* Opt-in `ack_generator:ack_json_schema` builder generates `@Schemable` schema
libraries and model parts from named draft 2020-12 `$defs` bundles in one
build. `unknown_properties: preserve` widens strict objects explicitly for
forward-compatible clients. Validated value-model fallback now covers valid
draft 2020-12 roots and definitions that cannot become typed Ack fields,
including recursion, dynamic references, composition, and nullable values.
`model_mode: validated` forces exact source-schema parse/encode semantics;
`assert_formats` and offline `documents` options retain format and external
resource behavior. Malformed or unresolved schemas fail with JSON-pointer
diagnostics.

## 1.7.0-beta.6

### Changed
Expand Down
81 changes: 81 additions & 0 deletions packages/ack_generator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,87 @@ come from `package:ack/annotations.dart`. `@AckInfer()` and `@AckModel()` are de
spellings of `@Schemable()` until 2.0.0. It also retains the deprecated Ack
1.1 `@AckType()` generator unchanged.

## Generate models from JSON Schema

The opt-in `ack_json_schema` builder turns a draft 2020-12 JSON Schema into
models. It emits typed `@AckInfer` models for shapes it can represent as Ack
fields. For other valid 2020-12 shapes, it emits validated
value models backed by `Ack.fromJsonSchema`, preserving every supported
assertion. Name the source `lib/contract.schema.json`; named `$defs` entries
produce models, and a schema with root assertions also produces `Root`
(`DocumentRoot` when `$defs/Root` already owns that name):

```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": {
"User": {
"type": "object",
"properties": { "name": { "type": "string" } },
"required": ["name"],
"additionalProperties": false
}
}
}
```

Enable the builder in the consuming package's `build.yaml`:

```yaml
targets:
$default:
builders:
ack_generator:ack_json_schema:
enabled: true
options:
unknown_properties: schema
assert_formats: false
model_mode: auto # use validated for exact source-schema parity
# Optional offline resources: retrieval URI -> package asset path.
# documents:
# "https://example.test/common.json": "lib/common.json"
```

Run `dart run build_runner build`. Import `contract.schema.dart` to use the
generated models. A typed model also writes `contract.schema.ack.dart` and
`contract.schema.ack.g.dart` and offers typed fields and `copyWith`. A validated
value model needs no parts: `User.parse(value)` returns a wrapper whose
immutable `value` is the validated JSON value; `User.encode(model)`,
`User.safeEncode(model)`, and `model.toJson()` revalidate on output. The
`userSchema` declaration and `User.schema` expose the exact source validator.
Do not hand-edit generated files.

The default `unknown_properties: schema` follows each object's
`additionalProperties`. Choose `preserve` for a forward-compatible client: it
widens `additionalProperties: false` to `true` at every object, records every
widened JSON pointer in the generated header, and captures unknown fields in
`additionalProperties`. The source JSON Schema is never rewritten. This is a
deliberate change to the effective validation contract.

Typed output covers named non-null objects, references to named definitions,
inline objects, maps, arrays with non-null items, common primitives and bounds,
string enums, and simple unions. Other valid shapes—including recursive and
dynamic references, composition, nullable roots and items, `multipleOf`,
unevaluated keywords, and format assertions—use validated value models.
`assert_formats: true` opts into format assertions under the standard dialect;
unknown asserted formats and unresolved or malformed schemas fail the build
with a JSON-pointer diagnostic. External resources are read only from the
explicit `documents` mapping; the builder never fetches URLs. The complete
required 2020-12 corpus generates offline with its supplied resources. Custom
vocabulary support is limited to what `Ack.fromJsonSchema` implements.

Definitions with non-Dart names receive stable UpperCamelCase wrapper names.
If the entire document uses fallback, all its models are value wrappers so
references and dynamic scope stay bound to the same effective source document.
Validated wrappers do not expose inferred fields or typed `copyWith`.

An integral JSON number beyond Dart's `int` range is a known edge case:
`Ack.fromJsonSchema` can accept it as an integral `double`, while a typed
`Ack.integer()` model cannot represent it as an `int`. String length bounds also
differ for astral Unicode characters (code points in the importer versus UTF-16
units in the fluent schema). Set `model_mode: validated` when these edge cases
matter; typed output retains the original v1 contract for compatibility.

## Schema-first usage

```dart
Expand Down
11 changes: 11 additions & 0 deletions packages/ack_generator/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,14 @@ builders:
auto_apply: none
build_to: source
required_inputs: [".ack.dart"]

ack_json_schema:
import: "package:ack_generator/builder.dart"
builder_factories: ["ackJsonSchemaBuilder"]
build_extensions: {".schema.json": [".schema.dart"]}
auto_apply: none
build_to: source
runs_before: ["ack_generator:ack_models"]
defaults:
options:
unknown_properties: schema
Loading
Loading