# Ack

Documentation for the Ack project

## Docs

### Overview

Source: https://docs.page/conceptadev/ack

```mdx

External data can't be trusted. APIs change, users mistype, and JSON drifts from what your code expects. Ack is a schema validation library for Dart and Flutter that puts a guardrail at those boundaries: describe the shape you expect once, then check untrusted data against it. You get a clear error when it doesn't match — and your data, validated, when it does.

("Ack" is short for "acknowledgment.")

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

final userSchema = Ack.object({
  'name': Ack.string().minLength(2).maxLength(50),
  'age': Ack.integer().min(0).max(120),
  'email': Ack.string().email().nullable(),
});

final result = userSchema.safeParse({
  'name': 'Ada',
  'age': 36,
  'email': 'ada@example.com',
});

if (result.isOk) {
  print('Welcome, ${result.getOrThrow()!['name']}');
} else {
  print(result.getError()); // tells you exactly which field failed, and why
}
```

On success, `getOrThrow()` returns your **validated data** as a `Map<String, Object?>`. Ack checks and shapes the data — opt into [code generation](/core-concepts/typesafe-schemas) when you'd rather have typed getters than map access.

## Why Ack?

- **Guard your boundaries.** Catch bad API responses, user input, and config before they reach your logic.
- **Define each shape once.** Reuse one schema for validation, JSON Schema export, and typed models.
- **Errors you can act on.** Every failure points at the exact field and the reason it failed.
- **Just Dart.** A fluent API with no required build step — reach for code generation only when you want it.

## What do you want to do?

- **Validate an API response** — [Quickstart Tutorial](/getting-started/quickstart-tutorial), then [JSON Serialization](/core-concepts/json-serialization)
- **Show field errors in a Flutter form** — [Flutter Form Validation](/guides/flutter-form-validation)
- **Make a field optional or nullable** — [Optional vs nullable](/core-concepts/schemas#optional-vs-nullable)
- **Parse dates, URIs, and durations** — [Codecs](/core-concepts/codecs)
- **Add custom validation logic** — [Custom Validation](/guides/custom-validation)
- **Generate typed models (no manual casts)** — [TypeSafe Schemas](/core-concepts/typesafe-schemas)
- **Reuse and compose schemas** — [Common Recipes](/guides/common-recipes)

## Install

```bash
dart pub add ack
```

New here? The [Quickstart Tutorial](/getting-started/quickstart-tutorial) takes you from install to handling every validation outcome in a few minutes.

## Learn more

- [Schema Types](/core-concepts/schemas) — every schema type and how to compose them
- [Validation Rules](/core-concepts/validation) — built-in constraints
- [Error Handling](/core-concepts/error-handling) — read and display structured errors
- [JSON Serialization](/core-concepts/json-serialization) — validate and encode JSON
- [API Reference](/api-reference/) — a core API quick reference with a link to generated API docs

*Building with an AI agent? Start at [`/llms.txt`](/llms.txt) for a compact, machine-readable index.*
```

### API Reference

Source: https://docs.page/conceptadev/ack/api-reference

```mdx

This page is a curated quick reference for the core Ack classes, methods, and
annotations. Use the [generated API documentation](https://pub.dev/documentation/ack/latest/ack/)
for every public declaration and exact signatures; use the linked guides here
for explanations and examples.

## Core `Ack` Class

Entry point for creating schemas. See [Schema Types](../core-concepts/schemas.mdx).

- `Ack.string()`: Creates a `StringSchema` for validating `String` values.
- `Ack.integer()`: Creates an `IntegerSchema` for validating `int` values. Rejects `double` (e.g. `42.0`).
- `Ack.double()`: Creates a `DoubleSchema` for validating `double` values. Rejects `int` (e.g. `42`).
- `Ack.number()`: Creates a `NumberSchema` for validating any `num` value (accepts both `int` and `double`).
- `Ack.boolean()`: Creates a `BooleanSchema` for validating `bool` values.
- `Ack.list(AckSchema itemSchema)`: Creates a `ListSchema` for validating arrays. Nullable item schemas are not supported; make the list itself nullable instead.
- `Ack.object(Map<String, AckSchema> properties)`: Creates an `ObjectSchema` for validating objects.
- `Ack.enumValues(List<T> values)`: Creates an `EnumSchema<T>` for Dart enum
  types. Parses enum `.name` strings into typed enum values. Pass typed enum
  values to `encode` or `safeEncode` for the reverse direction. **Preferred
  over `enumString` when a Dart enum exists.**
- `Ack.enumCodec(List<T> values)`: Like `enumValues`, but returns a
  `CodecSchema<String, T>` instead of an `EnumSchema<T>`. Use this when
  downstream code expects every value-shape to be a `CodecSchema` (e.g. a
  registry of codecs). The decode and encode functions are identity — the
  underlying `EnumSchema` still maps between `T` and the enum's `.name`.
- `Ack.enumString(List<String> values)`: Creates a `StringSchema` constrained
  to the given values. For ad-hoc string lists without a backing Dart enum.
- `Ack.anyOf(List<AckSchema> schemas)`: Creates an `AnyOfSchema` for union types.
- `Ack.any()`: Creates an `AnySchema` that accepts any non-null JSON-safe
  value. Chain `.nullable()` to allow `null`.
- `Ack.instance<T extends Object>()`: Creates a schema that checks a value is a
  Dart instance of `T` (runtime type check; handy as a codec `output` schema).
- `Ack.codec(...)` / `Ack.date()` / `Ack.datetime()` / `Ack.uri()` /
  `Ack.duration()` / `Ack.enumCodec(...)`: Create codecs. See
  [Codecs](../core-concepts/codecs.mdx).
- `Ack.lazy(String name, AckSchema Function() builder, {int maxDepth = 100})`:
  Creates a memoized deferred schema reference for recursive schema graphs.
  `maxDepth` must be at least `1` and limits parsing, runtime validation, and
  encoding. JSON Schema export renders Draft-7 `definitions` / `$ref` entries
  using `name` but warns that the runtime-only depth limit was omitted.
- `Ack.discriminated<T extends Object>(...)`: Creates a discriminated union
  schema. Branches may be plain `ObjectSchema` or transformed schemas whose
  base is an `ObjectSchema`. The union owns the discriminator: branches normally
  omit it, boundary payloads must include it, compatible branch discriminator
  fields are allowed, and conflicts are rejected. Exported/generated branches
  expose the exact branch literal.

## `AckSchema<Boundary, Runtime>` (Base Class)

Base class for all schema types.

### Primary Validation Methods

- `SchemaResult<Runtime> safeParse(Object? data, {String? debugName})`: Validates
  `data` and returns a `SchemaResult`. Invalid input and recoverable `Exception`
  values thrown by constraint/refinement callbacks become failures. `Error`
  values from those callbacks are rethrown with their original stack trace.
  Codec/transform decoder failures, including `Error` values, become
  `SchemaTransformError` failures.
- `Runtime? parse(Object? data, {String? debugName})`: Validates `data` and returns the value; throws `AckException` on failure.
- `SchemaResult<TOut> safeParseAs<TOut extends Object>(Object? data, TOut Function(Runtime?) map, {String? debugName})`: Parses and maps the validated value to `TOut`. Mapper failures, including `Error` values, become `SchemaTransformError` failures.
- `TOut parseAs<TOut extends Object>(Object? data, TOut Function(Runtime?) map, {String? debugName})`: Throwing variant of `safeParseAs`.
- `SchemaResult<Boundary> safeEncode(Runtime? value, {String? debugName})`: Encodes a runtime value to the boundary representation.
- `Boundary? encode(Runtime? value, {String? debugName})`: Throwing variant of `safeEncode`.

### Schema Modification Methods

- `AckSchema<Boundary, Runtime> nullable({bool value = true})`: Returns a new schema that also accepts `null`.
- `AckSchema<Boundary, Runtime> optional({bool value = true})`: Returns a new schema marked as optional (for object fields).
- `AckSchema<Boundary, Runtime> describe(String description)`: Attaches a description for documentation and JSON Schema generation.
- `DefaultSchema<Boundary, Runtime> withDefault(Runtime value)`: Wraps the schema in a `DefaultSchema` that supplies `value` when the parse input is `null`.

Primitive schemas (`StringSchema`, `IntegerSchema`, `DoubleSchema`, `NumberSchema`, `BooleanSchema`) are strict — they reject values whose Dart runtime type doesn't match. `IntegerSchema` and `DoubleSchema` do not overlap (`42.0` fails `Ack.integer()`, `42` fails `Ack.double()`); use `Ack.number()` when either is acceptable. For non-`num` boundary types (e.g. numeric strings), use [`transform`](../core-concepts/schemas.mdx#transformations) or [`codec`](#codecschemaboundary-runtime) to convert before validation.

### Custom Validation Methods

- `AckSchema<Boundary, Runtime> constrain(Constraint<Runtime> constraint, {String? message})`: Adds a constraint and optionally overrides its message. The constraint must mix in `Validator<Runtime>`, or an `ArgumentError` is thrown.
- `AckSchema<Boundary, Runtime> withConstraint(Constraint<Runtime> constraint)`: Adds a constraint directly (no message override; `constrain` delegates here).
- `AckSchema<Boundary, Runtime> refine(bool Function(Runtime) validate, {String message = 'The value did not pass the custom validation.'})`: Adds a custom validation predicate with an optional error message.
- `CodecSchema<Boundary, R> transform<R>(R Function(Runtime) transformer)`: Transforms validated runtime values to `R` (parse-only; encode fails).

### Utility Methods

- `Map<String, Object?> toJsonSchema()`: Returns a Draft-7 JSON Schema map via the canonical `AckSchemaModel` boundary.
- `AckSchemaModel toSchemaModel()`: Returns the canonical, target-independent
  boundary model for schema adapters, including export warnings.
- `Map<String, Object?> toMap()`: Serializes the schema for debugging.

See also [Schema Types](../core-concepts/schemas.mdx) for detailed usage examples.

## `StringSchema`

Schema for validating strings. See [String Validation](../core-concepts/validation.mdx#string-constraints).

### Length Constraints

- `minLength(int min)`: Minimum string length
- `maxLength(int max)`: Maximum string length
- `length(int exact)`: Exact string length
- `notEmpty()`: String must not be empty (equivalent to `minLength(1)`)

### Pattern Matching

- `matches(String pattern, {String? example, String? message})`: Must match a regex pattern. Patterns are not automatically anchored — use `^...$` for full-string matching. See [String validation](../core-concepts/validation.mdx#string-constraints) for details.
- `contains(String pattern, {String? example, String? message})`: Must contain the pattern anywhere in the string.
- `startsWith(String value)`: Must start with `value`.
- `endsWith(String value)`: Must end with `value`.

### Format Validation

- `email()`: Must be valid email format
- `url()`: Must be valid URL format (alias for `uri()`)
- `uri()`: Must be a valid absolute URI with a scheme and host
- `uuid()`: Must be valid UUID format
- `ip({int? version})`: Must be valid IP address (version 4 or 6)
- `ipv4()`: Must be valid IPv4 address
- `ipv6()`: Must be valid IPv6 address

### Date and Time

- `date()`: Must be valid ISO 8601 date (YYYY-MM-DD)
- `datetime()`: Must be a valid ISO 8601 datetime; announced RFC leap seconds
  are accepted and preserved as strings
- `time()`: Must be valid time format (HH:MM:SS)

### Transformations

- `trim()`: Removes leading and trailing whitespace
- `toLowerCase()`: Converts to lowercase
- `toUpperCase()`: Converts to uppercase

## `IntegerSchema` / `DoubleSchema` / `NumberSchema` (Number Schemas)

Schemas for validating numeric values. `IntegerSchema` only accepts `int`,
`DoubleSchema` only accepts `double`, and `NumberSchema` accepts any `num`
(either `int` or `double`). `DoubleSchema` and `NumberSchema` reject non-finite
values by default. See [Number Validation](../core-concepts/validation.mdx#number-constraints).

Each method's parameter type matches the schema's runtime type: `int` for `IntegerSchema`, `double` for `DoubleSchema`, and `num` for `NumberSchema`.

- `min(N limit)`: Minimum value (inclusive)
- `max(N limit)`: Maximum value (inclusive)
- `greaterThan(N limit)`: Must be greater than limit (exclusive)
- `lessThan(N limit)`: Must be less than limit (exclusive)
- `positive()`: Must be greater than 0
- `negative()`: Must be less than 0
- `multipleOf(N factor)`: Must be a multiple of the factor
- `finite()`: Must be finite (`DoubleSchema` and `NumberSchema`; already the default)
- `safe()`: Must be within safe integer range (`IntegerSchema` only)

## `BooleanSchema`

Schema for validating booleans. Validates `true` and `false` values strictly — non-boolean inputs are rejected. For boundary types that arrive as strings (e.g. `"true"`/`"false"`), use a `transform` or `codec` to convert before validation.

## `ListSchema<T>`

Schema for validating arrays. See [List Validation](../core-concepts/validation.mdx#list-constraints).

- `minItems(int min)`: Minimum number of items (alias: `minLength`)
- `maxItems(int max)`: Maximum number of items (alias: `maxLength`)
- `exactLength(int exact)`: Exact number of items (alias: `length`)
- `nonEmpty()`: List must have at least one item (alias: `notEmpty`)
- `unique()`: All items must be unique

## `ObjectSchema`

Schema for validating objects (maps). See [Object Validation](../core-concepts/schemas.mdx#object).

- Constructed using `Ack.object(Map<String, AckSchema> properties, {bool additionalProperties = false})`.
- Use `.pick(List<String> keys)` to create schema with only specified properties.
- Use `.omit(List<String> keys)` to create schema excluding specified properties.
- Use `.extend(Map<String, AckSchema> newProperties)` to add more properties.
- Use `.partial()` to make all properties optional.
- Use `.strict()` to disallow additional properties.
- Use `.passthrough()` to allow additional properties not defined in the schema.
- Use `.merge(ObjectSchema other)` to combine with another object schema.

## `SchemaResult<T>`

Object returned by `safeParse()`. See [Error Handling](../core-concepts/error-handling.mdx).

- `bool isOk`: `true` if validation succeeded.
- `bool isFail`: `true` if validation failed.
- `T? getOrThrow()`: Returns the validated value (which can be `null` for a nullable schema), or throws `AckException` on failure.
- `T? getOrNull()`: Returns the validated value, or `null` on failure.
- `SchemaError getError()`: Returns the validation error; only valid when `isFail` is `true`.
- `T? getOrElse(T? Function() orElse)`: Returns the validated value, or calls `orElse` on failure.
- `SchemaResult<R> map<R>(R Function(T?) transform)`: Maps the successful value to a new result type; propagates failures unchanged.
- `R match<R>({required R Function(T?) onOk, required R Function(SchemaError) onFail})`: Pattern-matches on success or failure.
- `void ifOk(void Function(T?) action)`: Executes `action` only when the result is successful.
- `void ifFail(void Function(SchemaError) action)`: Executes `action` only when the result is a failure.

## `SchemaError` (and subclasses)

Represents a validation failure. See [Error handling](../core-concepts/error-handling.mdx).

- `String message`: Human-readable error message.
- `SchemaContext context`: Context about where the error occurred.

**Subclasses:**
- `TypeMismatchError`: The input has the wrong Dart runtime type.
- `SchemaConstraintsError`: One or more constraint violations.
- `SchemaNestedError`: Validation failures in nested objects or arrays.
- `SchemaValidationError`: Custom refinement failures.
- `SchemaTransformError`: Decode/transform callback failures.
- `SchemaEncodeError`: Encode-path failures (non-nullable null, one-way transform, encoder threw, etc.).

## `Constraint<T>`

Base class for custom validation rules. See [Custom validation](../guides/custom-validation.mdx).

- `String constraintKey`: Unique identifier for the constraint.
- `String description`: Human-readable description.
- `Map<String, Object?> toMap()`: Serializes the constraint for debugging.

## `Validator<T>` (mixin)

Validation behavior mixin used with `Constraint<T>`.

- `bool isValid(T value)`: Returns `true` when the value passes validation.
- `String buildMessage(T value)`: Builds the validation failure message.
- `ConstraintError? validate(T value)`: Validates a value and returns an error if invalid.

## Additional schema types

Ack ships with a broad set of schema factories beyond what is listed here.
See [Schema types](../core-concepts/schemas.mdx) for the full catalogue,
including `Ack.date()`, `Ack.literal()`, and list/object combinators.

### `Ack.discriminated(...)`

Schema for polymorphic validation based on a string discriminator property.

- Branch schemas normally omit the discriminator field.
- The boundary payload must still contain the discriminator key.
- If a branch schema includes the discriminator field, it must be
  `Ack.literal(...)` matching the branch key or `Ack.enumString(...)`
  containing it. Broad, transformed, refined, or otherwise restrictive
  discriminator fields are rejected.
- Exported and generated schemas expose each branch discriminator as an exact
  literal.
- Generated subtype `parse()` and `safeParse()` methods validate through the
  union's effective branch.

### `Ack.lazy(...)`

Schema reference for recursive object graphs.

- Created using `Ack.lazy<Boundary, Runtime>(name, builder)`. `maxDepth`
  defaults to `100` and must be at least `1`; pass it explicitly only to
  override the default.
- The builder is resolved once and memoized.
- Exceeding `maxDepth` returns a validation failure during parsing, runtime
  validation, or encoding.
- `toJsonSchema()` and `toSchemaModel()` export Draft-7 `definitions` / `$ref`
  entries using the lazy `name`. The runtime-only depth limit cannot be
  represented by `$ref`, so exported schema models warn that it was omitted.
- Bare or wrapped lazy schemas cannot be used as discriminated-union branches
  because the branch discriminator must be analyzable at construction time.

## Code generation annotations

Use the [`ack_generator`](https://pub.dev/packages/ack_generator) builder to turn annotations into extension types. After adding the annotations below, run:

```bash
dart run build_runner build
```

### `@AckType()`

**Target**: Schema variables and getters

**Generates**: An extension type wrapper around the existing schema

Annotate a schema variable or getter to generate an extension type wrapper. The schema stays in your source file.

**Supported schema types:**
- `Ack.object({...})` → Object extension types
- Primitives: `Ack.string()`, `Ack.integer()`, `Ack.double()`, `Ack.boolean()`
- Collections: `Ack.list(...)`
- Enums: `Ack.literal()`, `Ack.enumString()`, `Ack.enumValues()`
- Discriminated unions: `Ack.discriminated(...)`

**Unsupported:** `Ack.any()`, `Ack.anyOf()`

For `Ack.discriminated(...)` constraints with `@AckType`, see
[Type-safe Schemas](../core-concepts/typesafe-schemas.mdx#discriminated-schemas).

**Example:**
```dart
@AckType()
final userSchema = Ack.object({
  'name': Ack.string(),
  'email': Ack.string().email(),
});

// Generated:
// - extension type UserType(Map<String, Object?> _data) { ... }
// - The schema variable remains unchanged

// Usage:
final user = UserType.parse({'name': 'Alice', 'email': 'alice@example.com'});
print(user.name);  // Type-safe String access
print(user.email); // Type-safe String access
```

### `EnumSchema<T>`

Schema for mapping enum `.name` strings at the boundary to typed enum values at
runtime. Created using `Ack.enumValues(List<T> values)` where `T extends Enum`;
`encode` and `safeEncode` map typed enum values back to their names.

### `AnyOfSchema`

Schema for union types; the value must match one of several schemas. Created using `Ack.anyOf(List<AckSchema> schemas)`.

### `DiscriminatedObjectSchema`

Schema for polymorphic validation based on a discriminator field.

- Created using `Ack.discriminated<T extends Object>(...)` with
  `discriminatorKey` and `schemas`.
- `effectiveBranch(String discriminatorValue)`: Returns the branch schema with
  the discriminator injected as the exact branch literal. Generated subtypes use
  this to validate a specific branch.

### `AckSchemaModel`

Canonical export model for Ack schemas.

- Created by `schema.toSchemaModel()`.
- Represents the boundary/export shape, JSON-compatible defaults, discriminator metadata, target-independent constraints, and export warnings.
- `schema.toSchemaModel().toJsonSchema()` returns the same Draft-7 map as `schema.toJsonSchema()`.
- Adapters that need a JSON map should call `schema.toJsonSchema()`; adapters for non-JSON targets should convert from `AckSchemaModel` rather than traversing `AckSchema` subclasses directly.

### `AnySchema`

Accepts any non-null JSON-safe value without further validation.

- Created by `Ack.any()`.
- Useful for dynamic payloads or pass-through metadata.

### `CodecSchema<Boundary, Runtime>`

Schema that decodes boundary values into runtime values and encodes runtime
values back to the boundary representation. Use `encode` / `safeEncode` for the
reverse direction. See [Codecs](../core-concepts/codecs.mdx).

- `schema.transform<R>(R Function(Runtime) transformer)`: one-way transform
  (parse only; `encode` fails with a one-way error).
- `schema.codec<R>({required R Function(Runtime) decode, required Runtime Function(R) encode, AckSchema<dynamic, R>? output})`:
  bidirectional codec; the optional `output` schema validates the runtime value.
- `Ack.codec<Boundary, InputRuntime, Runtime>({required input, required decode, required encode, output})`:
  builds a codec from an `input` schema.

**Built-in codecs:**

- `Ack.date()` → `CodecSchema<String, DateTime>` (ISO `YYYY-MM-DD` ↔ local-midnight `DateTime`)
- `Ack.datetime()` → `CodecSchema<String, DateTime>` (ISO 8601 ↔ UTC
  `DateTime`; leap-second strings are rejected because Dart cannot represent
  them)
- `Ack.uri()` → `CodecSchema<String, Uri>` (absolute URI string ↔ `Uri`)
- `Ack.duration()` → `CodecSchema<int, Duration>` (milliseconds ↔ `Duration`)
- `Ack.enumCodec(List<T> values)` → `CodecSchema<String, T>` (enum `.name` ↔ enum value)

### Optional schemas

Every schema can be marked optional via the `optional({bool value = true})` fluent API.

- `schema.optional()` sets `isOptional` to `true` without wrapping the schema.
- `schema.optional(value: false)` clears the optional flag.
- Optional affects object-field presence only; combine with `.nullable()` to also allow explicit `null`.

*See the [Schema types](../core-concepts/schemas.mdx) guide for detailed usage and examples.*
```

### llms.txt

Source: https://docs.page/conceptadev/ack/llms.txt

```mdx

This route redirects to the canonical static `llms.txt` file.
```

### Codecs

Source: https://docs.page/conceptadev/ack/core-concepts/codecs

```mdx

Your API sends a timestamp as an ISO string, but your code wants a `DateTime`. A codec handles that round-trip: it validates the incoming string, decodes it into the Dart type you actually use, and encodes it back when you send data out.

```dart
final when = Ack.datetime(); // CodecSchema<String, DateTime>

final dt = when.parse('2026-01-01T00:00:00Z'); // decode: String -> DateTime
final iso = when.encode(dt);                    // encode: DateTime -> String
```

Ack calls the wire shape the **boundary** (JSON-safe primitives like `String` or `int`) and the Dart value the **runtime** (a `DateTime`, `Uri`, `Duration`, or your own type). Parsing decodes boundary → runtime; encoding goes the other way.

## Transform vs. codec

| Helper | Direction | Returns |
| --- | --- | --- |
| `schema.transform<R>(fn)` | one-way (parse only) | `CodecSchema<Boundary, R>` |
| `schema.codec<R>(decode: ..., encode: ...)` | bidirectional | `CodecSchema<Boundary, R>` |

Use `transform` when you only ever decode. Encoding a transformed schema fails
with a `SchemaEncodeError` (a one-way transform has no encoder). Use `codec`
when you need a reversible encode path.

## Built-in codecs

Ack ships codecs for the most common boundary conversions:

| Factory | Boundary ↔ Runtime | Runtime invariant |
| --- | --- | --- |
| `Ack.date()` | ISO `YYYY-MM-DD` `String` ↔ `DateTime` | local midnight (no time-of-day) |
| `Ack.datetime()` | ISO 8601 `String` ↔ `DateTime` | must be UTC; leap-second strings are rejected |
| `Ack.uri()` | `String` ↔ `Uri` | absolute URI (scheme + host) |
| `Ack.duration()` | milliseconds `int` ↔ `Duration` | whole milliseconds |
| `Ack.enumCodec(values)` | enum-name `String` ↔ enum value | one of `values` |

```dart
final schema = Ack.object({
  'startsAt': Ack.datetime(),
  'website': Ack.uri(),
  'timeout': Ack.duration(),
});

final event = schema.parse({
  'startsAt': '2026-01-01T09:00:00Z',
  'website': 'https://example.com',
  'timeout': 5000,
});

// event['startsAt'] is a DateTime (UTC)
// event['website']  is a Uri
// event['timeout']  is a Duration (5 seconds)
```

Each built-in enforces a runtime invariant on encode. `Ack.datetime()` requires
a UTC `DateTime`; convert local values with `.toUtc()` before encoding. It also
rejects RFC leap-second strings before decoding because Dart normalizes `:60`.
Use `Ack.string().datetime()` when leap-second text must be validated and
preserved.

## Custom codecs

Create a codec from any input schema with `Ack.codec(...)`, providing a `decode` function (boundary → runtime) and an `encode` function (runtime → boundary):

```dart
final csv = Ack.codec<String, String, List<String>>(
  input: Ack.string(),
  decode: (s) => s.split(','),
  encode: (list) => list.join(','),
);

csv.parse('a,b,c');          // ['a', 'b', 'c']
csv.encode(['a', 'b', 'c']); // 'a,b,c'
```

The three type arguments are the boundary type, the input schema's runtime type, and the final runtime type. The optional `output` schema validates the decoded value (defaults to a type check on the runtime type).

Call `.codec<R>(...)` on an existing schema to add a reversible conversion:

```dart
final trimmed = Ack.string().codec<String>(
  decode: (s) => s.trim(),
  encode: (s) => s,
);
```

## Encoding back to the boundary

Use `encode` (throws on failure) or `safeEncode` (returns a `SchemaResult`):

```dart
final result = Ack.datetime().safeEncode(DateTime.utc(2026));
if (result.isOk) {
  print(result.getOrThrow()); // 2026-01-01T00:00:00.000Z
}
```

Errors surface as typed [`SchemaError`](./error-handling.mdx) values:

- `SchemaTransformError` — a `decode` function threw during parsing.
- `SchemaEncodeError` — encoding failed (for example, a one-way `transform`, or a
  runtime invariant violation such as a non-UTC `DateTime` for `Ack.datetime()`).

## Codecs and JSON Schema

A codec exports the JSON Schema of its **boundary** (input) schema — that is the shape that crosses the wire. For example, `Ack.datetime()` exports as `string` with `format: date-time`. The runtime type and decode/encode logic are not part of the exported schema. See [JSON Serialization](./json-serialization.mdx).

## Next steps

- **[Schema Types](./schemas.mdx)**: Schema types and the `transform` helper
- **[Validation Rules](./validation.mdx)**: Built-in constraints
- **[Error Handling](./error-handling.mdx)**: `SchemaError` and `SchemaResult`
- **[JSON Serialization](./json-serialization.mdx)**: Exporting schemas
```

### Configuration

Source: https://docs.page/conceptadev/ack/core-concepts/configuration

```mdx

Ack has no global configuration object. All behavior is configured at the schema level through the fluent API.

## Schema-level configuration

Define behavior when you create your schemas.

### Optional and nullable values

Fields in [`Ack.object`](./schemas.mdx#object) are required and non-nullable by
default. Use `.optional()` when a field may be omitted, `.nullable()` when a
present field may be `null`, or both when either state is valid.

```dart
Ack.object({
  'id': Ack.integer(),
  'nickname': Ack.string().optional(),
  'middleName': Ack.string().nullable(),
  'avatarUrl': Ack.string().url().optional().nullable(),
});
```

See [Optional vs nullable](./schemas.mdx#optional-vs-nullable) for the complete
presence and nullability matrix.

### Additional properties

Control how extra fields are handled with the `additionalProperties` parameter in [`Ack.object`](./schemas.mdx#object).

```dart
// Reject extra properties (default)
Ack.object({'id': Ack.integer()});
// or explicitly:
Ack.object({'id': Ack.integer()}, additionalProperties: false);

// Allow extra properties
Ack.object({'id': Ack.integer()}, additionalProperties: true);
```

Use strict objects for data you own and want to detect drift in. At external
API boundaries, `additionalProperties: true` can make a schema resilient to
new response fields while still validating every field your application uses.

### Default values

Use `.withDefault(value)` when a `null` or missing object field should receive
a runtime value during parsing. The default must satisfy the schema's runtime
type and constraints.

```dart
final settingsSchema = Ack.object({
  'theme': Ack.enumString(['light', 'dark']).withDefault('light'),
  'pageSize': Ack.integer().min(1).max(100).withDefault(20),
});
```

Defaults change the parsed result; `.optional()` only changes whether an object
field is required.

### Schema metadata

Attach a description with `.describe()` when the schema is also used for JSON
Schema export or adapter packages.

```dart
final userIdSchema = Ack.string()
    .uuid()
    .describe('Stable identifier for a user account');
```

### Custom error messages

Built-in constraints provide default messages. APIs that accept `message:`,
such as `.matches()`, can replace that message inline. Use `.constrain()` to
provide a message for a reusable custom constraint. See [Custom error
messages](./error-handling.mdx#custom-error-messages).

```dart
final usernameSchema = Ack.string().matches(
  r'^[a-z0-9_]+$',
  message: 'Use lowercase letters, numbers, and underscores only.',
);
```

### Custom validation logic

Use `.constrain()` for reusable value-level checks and `.refine()` for cross-field rules — see [Custom Validation](../guides/custom-validation.mdx).

## Code generation

Annotate a top-level schema with `@AckType()` to generate a typed wrapper — see [TypeSafe Schemas](./typesafe-schemas.mdx) for setup and supported shapes.

## Related guides

- [Schema Types](./schemas.mdx) — factories, composition, and transformations
- [Validation Rules](./validation.mdx) — built-in constraints
- [Codecs](./codecs.mdx) — bidirectional boundary/runtime configuration
- [JSON Schema Integration](../guides/json-schema-integration.mdx) — exported metadata and defaults
```

### Error Handling

Source: https://docs.page/conceptadev/ack/core-concepts/error-handling

```mdx

When validation fails, you want to know what went wrong and where. `safeParse()` hands back a `SchemaResult` holding either the validated value or a `SchemaError`, so you can inspect failures without try/catch.

## The `SchemaResult` object

`safeParse()` returns a `SchemaResult<T>`. Its value is nullable — a nullable schema can succeed with `null`.

- `result.isOk`: `true` if validation succeeded.
- `result.isFail`: `true` if validation failed.
- `result.getOrThrow()`: Returns the validated value, or throws `AckException` on failure.
- `result.getOrNull()`: Returns the validated value, or `null` on failure.
- `result.getError()`: Returns the `SchemaError` on failure, or throws `StateError` if called on a success.
- `result.getOrElse(() => defaultValue)`: Returns the validated value on success, or calls the fallback on failure.
- `result.match(onOk: (value) => ..., onFail: (error) => ...)`: Collapses both cases into a single value.
- `result.map((value) => ...)`: Transforms the success value and leaves failures untouched.
- `result.ifOk((value) { ... })` / `result.ifFail((error) { ... })`: Runs a side effect for just one case.

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

final schema = Ack.string().minLength(5);
final result = schema.safeParse('abc');

if (result.isFail) {
  final error = result.getError();
  print('Validation failed: $error');

  // Try to get data (will throw)
  try {
    result.getOrThrow();
  } catch (e) {
    print('Caught exception: $e');
  }

  // Get a default value
  final dataOrDefault = result.getOrElse(() => 'default_string');
  print('Data or default: $dataOrDefault'); // Output: default_string
}
```

## Understanding `SchemaError`

`SchemaError` contains details about the validation failure.

- `error.name`: The context name at the failure point (for example, a field name or the `debugName` passed to `safeParse`).
- `error.value`: The value that failed validation.
- `error.schema`: The schema being validated against.
- `error.path`: The RFC 6901 JSON Pointer path to the failure (for example, `#/address/city`).

```dart
final userSchema = Ack.object({
  'name': Ack.string(),
  'age': Ack.integer().min(18),
  'address': Ack.object({
    'city': Ack.string()
  })
});

final invalidData = {
  'name': 'Test',
  'age': 15, // Fails min(18)
  'address': {
    'city': 123 // Fails Ack.string
  }
};

final result = userSchema.safeParse(invalidData);

if (result.isFail) {
  final error = result.getError();
  print('Error Name: ${error.name}');         // Example: 'address'
  print('Error Path: ${error.path}');         // Example: '#/address/city'
  print('Failed Value: ${error.value}');      // Example: the invalid data
  print('Full Error: $error');               // Complete error details

  // Note: The exact error structure (especially for nested errors) can vary.
  // Sometimes the top-level error might be SchemaNestedError,
  // and you might need to inspect its nested errors.
}
```

## Error types

Each `SchemaError` subtype carries specific information about why validation failed.

### `TypeMismatchError`

Raised when the input type doesn't match the expected schema type.

```dart
final schema = Ack.string();
final result = schema.safeParse(42); // Number instead of string

if (result.isFail) {
  final error = result.getError() as TypeMismatchError;
  print('Expected: ${error.expectedType}'); // "string"
  print('Actual: ${error.actualType}');     // "integer"
  print('Error: ${error.message}');          // "Expected string, got integer"
}
```

### `SchemaConstraintsError`

Raised when the value violates one or more validation constraints (such as `minLength`, `min`, or `email`).

```dart
final schema = Ack.string().minLength(5).email();
final result = schema.safeParse('abc');

if (result.isFail) {
  final error = result.getError() as SchemaConstraintsError;
  print('Constraint violations: ${error.constraints.length}');

  // Access individual failed constraints
  for (final constraintError in error.constraints) {
    print('- Constraint: ${constraintError.constraint}');
    print('- Message: ${constraintError.message}');
  }
}
```

### `SchemaNestedError`

Raised when validation fails within nested objects or lists. Contains a list of child errors.

```dart
final schema = Ack.object({
  'name': Ack.string().minLength(2),
  'age': Ack.integer().min(0),
});

final result = schema.safeParse({
  'name': 'J',    // Too short
  'age': -5,      // Negative
});

if (result.isFail) {
  final error = result.getError() as SchemaNestedError;
  print('Nested errors: ${error.errors.length}');

  // Recursively inspect nested errors
  for (final nestedError in error.errors) {
    print('- Field: ${nestedError.name}');
    print('- Error: ${nestedError.message}');
  }
}
```

### `SchemaValidationError`

Raised when custom validation logic added via `.refine()` fails.

```dart
final schema = Ack.integer().refine(
  (value) => value % 2 == 0,
  message: 'Must be an even number',
);

final result = schema.safeParse(3);

if (result.isFail) {
  final error = result.getError() as SchemaValidationError;
  print('Validation error: ${error.message}'); // "Must be an even number"
}
```

### `SchemaTransformError`

Raised when a `.transform()` callback throws an exception.

Callbacks receive the non-null validated runtime value; null handling belongs to the surrounding `nullable`/`withDefault` configuration. `SchemaTransformError` fires when the callback itself throws — for example, when the input passes validation but the conversion fails:

```dart
final schema = Ack.string().transform((value) {
  final parsed = int.tryParse(value);
  if (parsed == null) throw FormatException('Not numeric: $value');
  return parsed;
});

final result = schema.safeParse('not-a-number');

if (result.isFail) {
  final error = result.getError() as SchemaTransformError;
  print('Transform error: ${error.message}');
  print('Cause: ${error.cause}'); // Original exception
}
```

### `SchemaEncodeError`

Raised when `encode()` or `safeEncode()` can't turn a runtime value back into its boundary form — for example, encoding a one-way `transform`, or a value that breaks a codec's invariant (such as a non-UTC `DateTime` for `Ack.datetime()`). Its `kind` field says which case occurred.

### Working with error types

Use pattern matching to handle specific error types:

```dart
final result = schema.safeParse(data);

if (result.isFail) {
  final error = result.getError();

  switch (error) {
    case TypeMismatchError():
      print('Wrong type: expected ${error.expectedType}');
    case SchemaConstraintsError():
      print('Failed ${error.constraints.length} constraints');
    case SchemaNestedError():
      print('Nested validation failed at ${error.errors.length} locations');
    case SchemaValidationError():
      print('Custom validation failed: ${error.message}');
    case SchemaTransformError():
      print('Transformation failed: ${error.cause}');
    case SchemaEncodeError():
      print('Could not encode value: ${error.message}');
    default:
      print('Validation failed: ${error.message}');
  }
}
```

## Displaying errors in UI (Flutter example)

In a `TextFormField` validator, return the error string directly:

```dart
// Inside TextFormField validator
validator: (value) {
  final result = someSchema.safeParse(value);
  if (result.isFail) {
    return result.getError().toString();
  }
  return null;
}
```

*See the [Form Validation Guide](../guides/flutter-form-validation.mdx) for details.*

## Custom error messages

Built-in constraints ship with default messages. For a custom message, pass `message:` to `.refine()`, or attach a constraint with `.constrain(..., message: ...)` — see [Custom Validation](../guides/custom-validation.mdx).

```dart
final schema = Ack.string().refine(
  (value) => value.startsWith('ACK_'),
  message: 'Value must start with ACK_',
);

final result = schema.safeParse('nope');
if (result.isFail) {
  print(result.getError().message); // Value must start with ACK_
}
```

## Next steps

- **[Custom Validation](/guides/custom-validation)**: Create constraints with custom error messages
- **[Flutter Forms](/guides/flutter-form-validation)**: Display validation errors in Flutter form widgets
- **[Validation Rules](/core-concepts/validation)**: All built-in validation constraints
- **[Schema Types](/core-concepts/schemas)**: Schema types and their behavior
- **[Common Recipes](/guides/common-recipes)**: Practical error handling patterns
```

### JSON Serialization

Source: https://docs.page/conceptadev/ack/core-concepts/json-serialization

```mdx

Ack validates decoded JSON at your application boundary. On the way back out,
plain schemas produce JSON-native Dart values directly, while
[codecs](./codecs.mdx) encode rich runtime values such as `DateTime` and `Uri`
back to their wire representation.

## Validating JSON data

Validating incoming JSON data involves two steps:

1.  **Decode JSON:** Use `dart:convert` to parse the JSON string into a Dart object (usually `Map<String, dynamic>` or `List`).
2.  **Validate:** Pass the decoded object to your [Ack schema](./schemas.mdx)'s `safeParse()` method.

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

// Define userSchema using correct Ack API
final userSchema = Ack.object({
  'name': Ack.string(),
  'age': Ack.integer().min(0),
  'email': Ack.string().email().nullable(),
});

void processApiResponse(String jsonString) {
  // 1. Decode JSON string into a Dart object.
  // jsonDecode returns an unknown structure until the schema validates it.
  Object? jsonData;
  try {
    jsonData = jsonDecode(jsonString);
  } catch (e) {
    print('Failed to decode JSON: $e');
    return;
  }

  // 2. Validate the decoded data against your defined schema.
  final result = userSchema.safeParse(jsonData);

  if (result.isOk) {
    // Structure and types are valid.
    final validDataMap = result.getOrThrow();
    print('Valid JSON received: $validDataMap');

    // Pass the validated map to your own model layer,
    // or use an AckType-generated wrapper (see next section).
  } else {
    // Handle validation errors (see the Error Handling guide).
    print('Invalid JSON data: ${result.getError()}');
  }
}

// Example Usage
processApiResponse('{"name": "Alice", "age": 30, "email": "alice@example.com"}');
processApiResponse('{"name": "Bob", "age": -5}'); // Invalid: age fails min(0)
processApiResponse('{"age": 25}'); // Invalid: missing required field 'name'
processApiResponse('not valid json'); // Decoding error
```

*Learn more about [Error Handling](./error-handling.mdx).*

## Working with validated data

After successful validation, `result.getOrThrow()` returns a `Map<String, Object?>` whose structure and types match your schema. You can work with it directly, pass it into a model class, or use a generated typed wrapper:

```dart
final result = userSchema.safeParse(jsonData);

if (result.isOk) {
  final validData = result.getOrThrow();

  // Option 1: Work directly with the validated Map
  final name = validData['name'] as String;
  final age = validData['age'] as int;

  // Option 2: Pass the validated map into your own model layer
  // - Constructor: User(name: validData['name'], age: validData['age'])
  // - json_serializable: User.fromJson(Map<String, dynamic>.from(validData))
  // - freezed: User.fromJson(Map<String, dynamic>.from(validData))
  // - dart_mappable: UserMapper.fromMap(validData)
  // - Manual factory: User.fromMap(validData)
}
```

## Parsing JSON into typed wrappers

Once a schema is annotated with `@AckType()` (see [TypeSafe Schemas](./typesafe-schemas.mdx) for setup), parse JSON straight into typed getters — no manual casting:

```dart
import 'dart:convert';

final jsonData = jsonDecode('{"name": "Alice", "email": "alice@example.com"}');
final result = UserType.safeParse(jsonData);

if (result.isOk) {
  final user = result.getOrThrow()!;
  print(user.name);  // typed String
  print(user.email); // typed String?
}
```

## Encoding validated data

For schemas whose runtime values are already JSON-native (`String`, `num`,
`bool`, `List`, `Map`, and `null`), pass the validated value to `jsonEncode`:

```dart
final result = userSchema.safeParse({
  'name': 'Alice',
  'age': 30,
  'email': 'alice@example.com',
});

if (result.isOk) {
  final validData = result.getOrThrow()!;
  final jsonString = jsonEncode(validData);
  print(jsonString);
}
```

When a schema decodes boundary values into richer Dart types, encode through
the schema before calling `jsonEncode`. This applies each codec in the nested
structure and restores the JSON-safe boundary shape:

```dart
final eventSchema = Ack.object({
  'name': Ack.string(),
  'startsAt': Ack.datetime(),
});

final event = eventSchema.parse({
  'name': 'Launch',
  'startsAt': '2026-01-15T14:00:00Z',
});

final boundaryData = eventSchema.encode(event);
final jsonString = jsonEncode(boundaryData);
```

Use `safeEncode()` when you want a `SchemaResult` instead of an exception.
See [Codecs](./codecs.mdx) for runtime invariants and custom bidirectional
conversions.

## Key considerations

- **`dart:convert`:** Use `jsonDecode` and `jsonEncode` for JSON text. Ack
  validates values and encodes codec runtime values back to their boundary
  representation.
- **Type safety:** `jsonDecode` produces `dynamic`, but successful validation guarantees the structure and types of the resulting `Map<String, Object?>`.
- **Model conversion:** After validation, how you convert the validated map into an app model is up to you. Ack keeps validation and wrapper generation separate from your model layer.
```

### Schemas

Source: https://docs.page/conceptadev/ack/core-concepts/schemas

```mdx

A schema describes the shape your data should have. You build one with the `Ack` factory, then validate input against it. This page tours every schema type and how to compose them.

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

// Define schema
final userSchema = Ack.object({
  'name': Ack.string().minLength(2),
  'age': Ack.integer().min(0),
  'email': Ack.string().email(),
});

// Validate data
final result = userSchema.safeParse({
  'name': 'John',
  'age': 30,
  'email': 'john@example.com',
});

if (result.isOk) {
  final validData = result.getOrThrow();
  print('Valid: ${validData['name']}');
} else {
  print('Error: ${result.getError()}');
}
```

## Schema types

### String

```dart
// Basic string — primitives are strict by default and reject non-string values
final nameSchema = Ack.string();

// With constraints
final usernameSchema = Ack.string()
  .minLength(3)
  .maxLength(20)
  .matches(r'^[a-zA-Z0-9_]+$');

// Email validation
final emailSchema = Ack.string().email();

// URL validation
final websiteSchema = Ack.string().url();

// Date/datetime strings
final dateSchema = Ack.string().date(); // YYYY-MM-DD
final datetimeSchema = Ack.string().datetime(); // ISO 8601

// Enum values
enum Role { admin, user, guest }
final roleSchema = Ack.enumValues(Role.values);
```

> **`Ack.string().date()` vs `Ack.date()`:** `Ack.string().date()` checks the
> format and keeps the value a `String`. [`Ack.date()`](./codecs.mdx) (a codec)
> validates the same format but returns a `DateTime`. The same applies to
> `Ack.string().datetime()` vs `Ack.datetime()`, except announced leap seconds:
> the string schema preserves them, while the codec rejects them because Dart's
> `DateTime` cannot represent `:60`.

### Number

Numeric schemas are strict about their Dart runtime type. `Ack.integer()`
rejects `double` values (even whole ones like `42.0`); `Ack.double()` rejects
`int` values. Use `Ack.number()` when either is acceptable. `Ack.double()` and
`Ack.number()` reject non-finite values (`NaN` and infinities) by default.

```dart
// Integer validation (int only — 42.0 would fail)
final ageSchema = Ack.integer()
  .min(0)
  .max(120);

// Double validation (double only — 42 would fail)
final priceSchema = Ack.double()
  .positive()
  .multipleOf(0.5); // Use factors that avoid floating point rounding issues

// Either int or double
final amountSchema = Ack.number().positive();

final temperatureSchema = Ack.integer(); // Any integer
final scoreSchema = Ack.double().positive(); // > 0
final debtSchema = Ack.double().negative(); // < 0
```

### Boolean

```dart
final isActiveSchema = Ack.boolean();
```

### List

```dart
// List of strings
final tagsSchema = Ack.list(Ack.string());

// With constraints
final itemsSchema = Ack.list(Ack.string())
  .minLength(1)
  .maxLength(10)
  .unique();

// List of objects
final usersSchema = Ack.list(Ack.object({
  'id': Ack.integer(),
  'name': Ack.string(),
}));
```

### Object

The most common schema type for structured data:

```dart
final userSchema = Ack.object({
  'name': Ack.string(),
  'age': Ack.integer().min(0),
  'email': Ack.string().email(),
});
```

**Nested objects:**

```dart
final userSchema = Ack.object({
  'name': Ack.string(),
  'address': Ack.object({
    'street': Ack.string(),
    'city': Ack.string(),
    'zipCode': Ack.string().matches(r'^\d{5}$'),
  }),
});
```

**Working with validated data:**

```dart
final result = userSchema.safeParse(data);

if (result.isOk) {
  final validData = result.getOrThrow();

  // Type cast when accessing
  final name = validData['name'] as String;
  final address = validData['address'] as Map<String, Object?>;
  final city = address['city'] as String;
}
```

### Union types

Validate against multiple possible schemas:

```dart
// String or integer — primitive branches are strict, so the union won't
// silently coerce one into the other.
final idSchema = Ack.anyOf([
  Ack.string(),
  Ack.integer(),
]);

// Discriminated union (polymorphic data)
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(),
    }),
  },
);
```

The union owns the discriminator and injects the exact branch literal at
parse/export boundaries. Branch schemas usually omit the discriminator field.

### Recursive schemas

Use `Ack.lazy(...)` when a schema needs to refer to itself:

```dart
late final ObjectSchema categorySchema;

categorySchema = Ack.object({
  'name': Ack.string(),
  'children': Ack.list(
    Ack.lazy<JsonMap, JsonMap>('Category', () => categorySchema),
  ),
});
```

The lazy builder is resolved once and memoized. JSON Schema export renders the
reference through Draft-7 `definitions` / `$ref`, so recursive children are
referenced rather than inlined forever. `maxDepth` defaults to `100`, must be at
least `1`, and returns a validation failure when parsing, runtime validation, or
encoding exceeds the limit. Because the limit is runtime-only and cannot be
represented by `$ref`, exported schema models warn that it was omitted.

### Any

Accepts any non-null JSON-safe value without validation (use sparingly):

```dart
final flexibleSchema = Ack.object({
  'id': Ack.string(),
  'metadata': Ack.any(), // Any non-null JSON-safe value accepted
});
```

Use `Ack.any().nullable()` to also accept `null`.

## Optional vs nullable

**`.nullable()`** — Field must be present but can be `null`:

```dart
final userSchema = Ack.object({
  'name': Ack.string(),
  'middleName': Ack.string().nullable(),
});

// ✅ Valid
{'name': 'John', 'middleName': null}
{'name': 'John', 'middleName': 'Robert'}

// ❌ Invalid - middleName missing
{'name': 'John'}
```

**`.optional()`** — Field can be omitted (but is still validated when present):

```dart
final userSchema = Ack.object({
  'name': Ack.string(),
  'age': Ack.integer().optional(),
});

// ✅ Valid
{'name': 'John'} // age omitted
{'name': 'John', 'age': 30}

// ❌ Invalid
{'name': 'John', 'age': null} // Use .nullable() if null should be allowed
```

**Combining both** — Field can be missing or `null`:

```dart
final userSchema = Ack.object({
  'name': Ack.string(),
  'bio': Ack.string().optional().nullable(),
});

// All valid:
{'name': 'John'}
{'name': 'John', 'bio': null}
{'name': 'John', 'bio': 'Developer'}
```

## Object schema operations

### Extension

Add or override properties:

```dart
final baseSchema = Ack.object({
  'id': Ack.string(),
  'name': Ack.string(),
});

// Add properties
final extendedSchema = baseSchema.extend({
  'email': Ack.string().email(),
  'role': Ack.literal('admin'),
});

// Override properties
final modifiedSchema = baseSchema.extend({
  'name': Ack.string().optional(), // Make name optional
});
```

### Pick and omit

Select or exclude properties:

```dart
final fullSchema = Ack.object({
  'id': Ack.string(),
  'name': Ack.string(),
  'email': Ack.string().email(),
  'password': Ack.string(),
  'createdAt': Ack.string().datetime(),
});

// Pick specific fields
final publicSchema = fullSchema.pick(['id', 'name', 'email']);

// Omit sensitive fields
final safeSchema = fullSchema.omit(['password']);
```

### Partial

Make all properties optional:

```dart
final userSchema = Ack.object({
  'name': Ack.string(),
  'email': Ack.string().email(),
  'age': Ack.integer(),
});

// All fields become optional
final partialSchema = userSchema.partial();

// All valid:
partialSchema.safeParse({});
partialSchema.safeParse({'name': 'John'});
partialSchema.safeParse({'email': 'john@example.com', 'age': 30});
```

### Additional properties

By default, objects are **strict** and reject additional properties.

#### Using the constructor parameter

```dart
// Strict mode (default) - rejects additional properties
final strictSchema = Ack.object({
  'id': Ack.string(),
  'name': Ack.string(),
}); // additionalProperties: false is the default

strictSchema.safeParse({'id': '1', 'name': 'John', 'extra': 'value'}); // ❌ Fails

// Passthrough mode - allows additional properties
final flexibleSchema = Ack.object({
  'id': Ack.string(),
  'name': Ack.string(),
}, additionalProperties: true);

flexibleSchema.safeParse({'id': '1', 'name': 'John', 'extra': 'allowed'}); // ✅ Passes
```

#### Using extension methods

```dart
final baseSchema = Ack.object({
  'id': Ack.string(),
  'name': Ack.string(),
});

// Make strict (reject extra properties)
final strict = baseSchema.strict();
strict.safeParse({'id': '1', 'name': 'John', 'role': 'admin'}); // ❌ Fails

// Allow passthrough (accept extra properties)
final passthrough = baseSchema.passthrough();
passthrough.safeParse({'id': '1', 'name': 'John', 'role': 'admin'}); // ✅ Passes
```

**Common use cases:**

- **Strict mode**: API request validation and form validation where only known fields are allowed
- **Passthrough mode**: Dynamic data or cases where extra metadata is acceptable

## Custom validation

### Refinements

Add custom validation logic:

```dart
// Password confirmation
final passwordSchema = Ack.object({
  'password': Ack.string().minLength(8),
  'confirmPassword': Ack.string(),
}).refine(
  (data) => data['password'] == data['confirmPassword'],
  message: 'Passwords must match',
);

// Business logic validation
final orderSchema = Ack.object({
  'items': Ack.list(Ack.object({
    'price': Ack.double(),
    'quantity': Ack.integer(),
  })),
  'total': Ack.double(),
}).refine(
  (order) {
    final items = order['items'] as List;
    final calculatedTotal = items.fold<double>(0, (sum, item) {
      final itemMap = item as Map<String, Object?>;
      final price = itemMap['price'] as double;
      final qty = itemMap['quantity'] as int;
      return sum + (price * qty);
    });
    final total = order['total'] as double;
    return (calculatedTotal - total).abs() < 0.01;
  },
  message: 'Total must match sum of items',
);
```

### Transformations

Transform validated data after parsing:

```dart
// Transform to uppercase. The callback receives the non-null validated
// runtime value; nullable handling happens on the surrounding schema.
final upperSchema = Ack.string().transform((s) => s.toUpperCase());

// Add computed fields
final userWithAgeSchema = Ack.object({
  'name': Ack.string(),
  'birthYear': Ack.integer(),
}).transform((data) {
  final birthYear = data['birthYear'] as int;
  final age = DateTime.now().year - birthYear;
  return {...data, 'age': age};
});

// Type transformation
final dateSchema = Ack.string()
  .matches(r'^\d{4}-\d{2}-\d{2}$')
  .transform<DateTime>((s) => DateTime.parse(s));
```

## Validation result

Every `safeParse()` returns a `SchemaResult` holding the validated value or a `SchemaError`. See [Error Handling](./error-handling.mdx) for reading values and errors.

## Next steps

- **[Validation rules](./validation.mdx)**: All built-in constraints and strict parsing
- **[Error handling](./error-handling.mdx)**: Handle validation errors effectively
- **[Custom validation](../guides/custom-validation.mdx)**: Create custom constraints and refinements
- **[JSON serialization](./json-serialization.mdx)**: Validate and transform JSON data
- **[Common recipes](../guides/common-recipes)**: Practical schema patterns
```

### TypeSafe Schemas

Source: https://docs.page/conceptadev/ack/core-concepts/typesafe-schemas

```mdx

Tired of writing `data['name'] as String` after every parse? Annotate a top-level schema with `@AckType()` and run the generator once to get typed getters like `user.name`. The schema stays in your source file; the generator adds a typed wrapper around its validated representation.

## Overview

1. Define schemas with the Ack fluent API.
2. Annotate each top-level schema variable or getter with `@AckType()`.
3. Run `dart run build_runner build`.
4. Use the generated `TypeName.parse()` / `TypeName.safeParse()` helpers.

## Basic usage

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

part 'user_schema.g.dart';

@AckType()
final addressSchema = Ack.object({
  'street': Ack.string(),
  'city': Ack.string(),
});

@AckType()
final userSchema = Ack.object({
  'id': Ack.string(),
  'email': Ack.string().email().nullable(),
  'address': addressSchema,
});
```

The generated part file contains `AddressType` and `UserType` extension types, each with typed field getters and `parse()` / `safeParse()` static methods.

The type name drops a trailing `Schema` and adds `Type` (`userSchema` → `UserType`). Override it with `@AckType(name: 'AppUser')`, which generates `AppUserType`.

## Supported schema shapes

`@AckType()` supports:

- `Ack.object(...)`
- `Ack.string()`, `Ack.integer()`, `Ack.double()`, `Ack.boolean()`
- `Ack.list(...)`
- `Ack.literal(...)`, `Ack.enumString(...)`, `Ack.enumValues(...)`
- non-object transforms with explicit output types
- `Ack.discriminated(...)` with the constraints below

`Ack.any()` and `Ack.anyOf()` are not supported.

## Discriminated schemas

`Ack.discriminated(...)` works with `@AckType()` when all of the following hold:

- `schemas` is a non-empty map literal
- the base schema is non-nullable
- each branch is a top-level, non-nullable `@AckType` object schema in the same library
- branch schemas omit the discriminator field, or include it as `Ack.literal(...)` matching the branch key, or `Ack.enumString(...)` containing the branch key

Example:

```dart
@AckType()
final catSchema = Ack.object({
  'lives': Ack.integer(),
});

@AckType()
final dogSchema = Ack.object({
  'breed': Ack.string(),
});

@AckType()
final petSchema = Ack.discriminated(
  discriminatorKey: 'type',
  schemas: {
    'cat': catSchema,
    'dog': dogSchema,
  },
);
```

`Ack.discriminated(...)` owns the discriminator property. Boundary payloads must include the discriminator key; branch schemas should usually omit it. When a branch includes the discriminator field, it must be an exact literal or enum containing the branch key:

```dart
@AckType()
final catSchema = Ack.object({
  'type': Ack.literal('cat'), // allowed, but usually unnecessary
  'lives': Ack.integer(),
});
```

Conflicting discriminator fields, broad `Ack.string()`, and transformed or refined discriminator fields are rejected. Generated subtype `parse()` / `safeParse()` methods validate through the union's effective branch.

## Resolution rules

- Nested object fields must reference a named top-level schema — inline anonymous objects are rejected.
- `Ack.list(...)` element schemas must be statically resolvable.
- Cross-file references work for direct imports, prefixed imports, and re-exports.
- Unannotated object schema references fail generation rather than silently falling back to raw maps.
- Circular alias/reference chains fail generation with a clear error.

## Limitations

- `@AckType()` only works on top-level schema variables and getters.
- Nullable top-level schemas do not emit extension types.
- `Ack.list(...)` rejects nullable item schemas. Make the list itself nullable
  with `Ack.list(item).nullable()` when the whole field may be null.
- Use `.transform<T>(...)` with an explicit output type so the generator can infer the representation type.

## Build checklist

1. Add `ack_annotations`, `ack_generator`, and `build_runner` to your pubspec.
2. Add `part '<file>.g.dart';` to the file.
3. Annotate top-level schema variables or getters with `@AckType()`.
4. Run `dart run build_runner build`.

## Next steps

- [JSON Serialization](./json-serialization.mdx) — parse JSON straight into generated types
- [Common Recipes](../guides/common-recipes.mdx) — patterns that combine schemas and generated types
- [API Reference](../api-reference/index.mdx) — core API quick reference and generated API docs
```

### Validation Rules

Source: https://docs.page/conceptadev/ack/core-concepts/validation

```mdx

Constraints turn "a string" into "an email between 2 and 50 characters." Chain them onto any schema — each comes with a sensible default error message, and you can supply your own (see [Custom Validation](../guides/custom-validation.mdx)).

## Common constraints

### `nullable()` and `optional()`

`.nullable()` lets a present field hold `null`; `.optional()` lets it be omitted. These are field-presence modifiers rather than constraints — see [Optional vs nullable](./schemas.mdx#optional-vs-nullable).

### `constrain(Constraint<T> constraint, {String? message})`

Applies a custom `Constraint<T>` that also implements `Validator<T>`. See the [Custom Validation](../guides/custom-validation.mdx) guide.

## String constraints

Apply these to [`Ack.string()`](./schemas.mdx#string) schemas.

### `minLength(int min)`
Requires at least `min` characters.
```dart
Ack.string().minLength(5)
```

### `maxLength(int max)`
Requires at most `max` characters.
```dart
Ack.string().maxLength(100)
```

### `length(int n)`
Requires exactly `n` characters.
```dart
Ack.string().length(10)
```

### `notEmpty()`
Requires a non-empty string. Equivalent to `minLength(1)`.
```dart
Ack.string().notEmpty()
```

### `matches(String pattern, {String? example, String? message})`
Requires the string to match a regular expression `pattern`.

Patterns are **not** automatically anchored. The pattern matches if found anywhere in the string. To require a full-string match, add anchors: `^...$`

```dart
// Simple alphanumeric pattern (full-string match with anchors)
Ack.string().matches(r'^[a-zA-Z0-9]+$')

// UUID pattern (full-string match with anchors)
Ack.string()
    .matches(r'^[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$')

// Without anchors - matches substring (usually not what you want!)
Ack.string().matches(r'[0-9]+')  // Matches "abc123xyz" ⚠️
```

### `contains(String pattern, {String? example, String? message})`
Requires `pattern` to appear somewhere in the string.
```dart
// Password must contain at least one uppercase letter
Ack.string().contains(r'[A-Z]')

// Password must contain at least one digit
Ack.string().contains(r'[0-9]')
```

### `email()`
Requires a valid email address format.
```dart
Ack.string().email()
```

### `date()`
Requires a valid date string in `YYYY-MM-DD` format.
```dart
Ack.string().date()
```

### `datetime()`
Requires a valid ISO 8601 date-time string with a timezone. Announced RFC 3339
leap seconds are accepted and preserved because this schema returns the input
`String`. The `Ack.datetime()` codec rejects leap seconds because Dart's
`DateTime` cannot represent them without normalization.
```dart
Ack.string().datetime()
```

### `time()`
Requires a valid time string in `HH:MM:SS` format.
```dart
Ack.string().time()
```

### `uri()`
Requires a valid absolute URI with a scheme and host. This is the network-URI
subset Ack accepts, rather than every relative or non-host URI permitted by RFC
3986.
```dart
Ack.string().uri()
```

### `uuid()`
Requires a valid UUID per RFC 4122.
```dart
Ack.string().uuid()
```

### `ipv4()`
Requires a valid IPv4 address.
```dart
Ack.string().ipv4()
```

### `ipv6()`
Requires a valid IPv6 address.
```dart
Ack.string().ipv6()
```



### `Ack.enumString(List<String> allowedValues)`
An `Ack` factory (not a fluent method) that creates a `StringSchema` accepting only values in `allowedValues`. Use for ad-hoc string lists; prefer `Ack.enumValues(MyEnum.values)` when a Dart enum exists.
```dart
Ack.enumString(['active', 'inactive', 'pending'])
```

### `Ack.literal(String value)`
An `Ack` factory (not a fluent method) that creates a `StringSchema` requiring an exact string match.
```dart
Ack.literal('admin')
```

### `startsWith(String prefix)`
Requires the string to start with `prefix`.
```dart
Ack.string().startsWith('https://')
```

### `endsWith(String suffix)`
Requires the string to end with `suffix`.
```dart
Ack.string().endsWith('.dart')
```

### `url()`
Alias for `uri()`. Requires a valid URL.
```dart
Ack.string().url()
```

### `ip({int? version})`
Requires a valid IP address. Set `version` to `4` or `6` to restrict the version.
```dart
Ack.string().ip()           // Any IP (v4 or v6)
Ack.string().ip(version: 4) // IPv4 only
Ack.string().ip(version: 6) // IPv6 only
```

## String transformations

These methods modify the output value during parsing rather than adding a validation constraint.

### `trim()`
Removes leading and trailing whitespace.
```dart
Ack.string().trim()
// "  hello  " → "hello"
```

### `toLowerCase()`
Converts the string to lowercase.
```dart
Ack.string().toLowerCase()
// "HELLO" → "hello"
```

### `toUpperCase()`
Converts the string to uppercase.
```dart
Ack.string().toUpperCase()
// "hello" → "HELLO"
```

## Number constraints

Apply these to [`Ack.integer()`](./schemas.mdx#number), [`Ack.double()`](./schemas.mdx#number), or [`Ack.number()`](./schemas.mdx#number) schemas.

### `min(num limit)`
Requires a value `>= limit` (inclusive).
```dart
Ack.integer().min(0) // >= 0
Ack.double().min(0.0) // >= 0.0
Ack.number().min(0) // >= 0
```

### `max(num limit)`
Requires a value `<= limit` (inclusive).
```dart
Ack.integer().max(100) // <= 100
Ack.double().max(100.0) // <= 100.0
Ack.number().max(100) // <= 100
```

### `greaterThan(num limit)`
Requires a value strictly `> limit` (exclusive).
```dart
Ack.integer().greaterThan(0) // > 0
Ack.double().greaterThan(0.0) // > 0.0
Ack.number().greaterThan(0) // > 0
```

### `lessThan(num limit)`
Requires a value strictly `< limit` (exclusive).
```dart
Ack.integer().lessThan(100) // < 100
Ack.double().lessThan(100.0) // < 100.0
Ack.number().lessThan(100) // < 100
```

### `multipleOf(num factor)`
Requires a value that is a multiple of `factor`. Integer checks are exact; doubles use a small scale-relative tolerance, so round accumulated floating-point sums (or validate integer units such as cents) before parsing.
```dart
Ack.integer().multipleOf(5) // Must be divisible by 5
Ack.double().multipleOf(0.5) // Use factors that avoid floating point rounding issues
Ack.number().multipleOf(0.5)
```

### `positive()`
Requires a value greater than 0.
```dart
Ack.integer().positive() // > 0
Ack.double().positive() // > 0.0
Ack.number().positive() // > 0
```

### `negative()`
Requires a value less than 0.
```dart
Ack.integer().negative() // < 0
Ack.double().negative() // < 0.0
Ack.number().negative() // < 0
```

### `safe()` (integer only)
Requires an integer within JavaScript's safe range (`-2^53+1` to `2^53-1`).
```dart
Ack.integer().safe()
```

### `finite()`
Requires a finite number (rejects `infinity` and `NaN`). `Ack.double()` and
`Ack.number()` enforce this by default; `finite()` is available for
explicitness and API symmetry.
```dart
Ack.double().finite()
Ack.number().finite()
```

## List constraints

Apply these to [`Ack.list()`](./schemas.mdx#list) schemas.

### `minLength(int min)`
Requires at least `min` items.
```dart
Ack.list(Ack.string()).minLength(1)
```

### `maxLength(int max)`
Requires at most `max` items.
```dart
Ack.list(Ack.integer()).maxLength(10)
```

### `length(int count)`
Requires exactly `count` items.
```dart
Ack.list(Ack.boolean()).length(5)
```

### `notEmpty()`
Requires a non-empty list. Equivalent to `minLength(1)`.
```dart
Ack.list(Ack.object({})).notEmpty()
```

### `unique()`
Requires all items to be unique. Uses deep structural equality, so nested maps and lists are compared by value.
```dart
Ack.list(Ack.string()).unique()
```

## Primitive type strictness

Primitive schemas (`Ack.string()`, `Ack.integer()`, `Ack.double()`, `Ack.number()`, `Ack.boolean()`) are strict: a value must already match the expected Dart runtime type. Mismatched inputs surface as a `TypeMismatchError` rather than being silently coerced.

Each schema maps to a specific runtime type:

| Schema           | Accepted runtime type | Notes                              |
| ---------------- | --------------------- | ---------------------------------- |
| `Ack.string()`   | `String`              | rejects `num`, `bool`, etc.        |
| `Ack.integer()`  | `int`                 | rejects `double` (even `42.0`)     |
| `Ack.double()`   | `double`              | rejects `int` (even `42`)          |
| `Ack.number()`   | `num`                 | accepts both `int` and `double`    |
| `Ack.boolean()`  | `bool`                | rejects `"true"`, `1`, `0`, etc.   |

```dart
final stringSchema = Ack.string();
stringSchema.safeParse('hello');  // ✅ OK
stringSchema.safeParse(123);      // ❌ FAIL: TypeMismatchError
stringSchema.safeParse(true);     // ❌ FAIL: TypeMismatchError

final intSchema = Ack.integer();
intSchema.safeParse(42);    // ✅ OK
intSchema.safeParse('42');  // ❌ FAIL: TypeMismatchError
intSchema.safeParse(42.0);  // ❌ FAIL: TypeMismatchError (double is not int)

final doubleSchema = Ack.double();
doubleSchema.safeParse(3.14);  // ✅ OK
doubleSchema.safeParse(42);    // ❌ FAIL: TypeMismatchError (int is not double)

final numberSchema = Ack.number();
numberSchema.safeParse(42);    // ✅ OK (int is num)
numberSchema.safeParse(3.14);  // ✅ OK (double is num)
numberSchema.safeParse('42');  // ❌ FAIL: TypeMismatchError
```

Because `Ack.integer()` and `Ack.double()` do not overlap, use `Ack.number()` when a field may be either an `int` or a `double`. Use `transform`/`codec` only when the boundary value isn't already a `num` (for example, a numeric string).

This strictness makes `anyOf` and discriminated unions reliable — they can distinguish the string `"42"` from the integer `42` without configuration:

```dart
final stringOrNumber = Ack.anyOf([
  Ack.string(),
  Ack.integer(),
]);

stringOrNumber.safeParse('42');  // ✅ Matches string branch
stringOrNumber.safeParse(42);    // ✅ Matches integer branch
```

### Converting boundary types

When your boundary payload uses a different shape than your runtime model (for example, ISO strings → `DateTime`, or `"true"`/`"false"` → `bool`), express the conversion explicitly with [`transform`](./schemas.mdx#transformations) or a `codec`:

```dart
// Boundary "true"/"false" string → runtime bool
final boolFromString = Ack.enumString(['true', 'false'])
  .transform((s) => s == 'true');

boolFromString.safeParse('true');   // ✅ runtime value: true
boolFromString.safeParse('false');  // ✅ runtime value: false
boolFromString.safeParse(true);     // ❌ FAIL: string schema rejects bool
```

Use `schema.codec<R>(decode: ..., encode: ...)` when you also need a reversible encode path back to the boundary type.

## Combining constraints

You can chain multiple constraints. Ack evaluates them in the order you apply them.

```dart
final usernameSchema = Ack.string()
  .minLength(3)           // First: check min length
  .maxLength(20)          // Second: check max length
  .matches(r'^[a-z0-9_]+$') // Third: require only lowercase letters, digits, or underscores
  .notEmpty();            // Redundant if minLength(>0) is used, but illustrates chaining

final quantitySchema = Ack.integer()
  .min(1)         // Must be at least 1
  .max(100)       // Must be at most 100
  .multipleOf(1); // Must be an integer (redundant for Ack.integer)
```

## Next steps

- **[Error handling](/core-concepts/error-handling)**: Handle and display validation errors
- **[Custom validation](/guides/custom-validation)**: Create custom constraints and refinement logic
- **[Schema types](/core-concepts/schemas)**: All available schema types and their operations
- **[Common recipes](/guides/common-recipes)**: Practical validation patterns and solutions
- **[Flutter forms](/guides/flutter-form-validation)**: Integrate validation with Flutter form widgets
```

### Installing Ack

Source: https://docs.page/conceptadev/ack/getting-started/installation

```mdx

Ack is a pure-Dart package with no required build step. Add the core library — and, optionally, the code generator for typed wrappers.

## Add to your project

Add Ack to your project using the Dart CLI:

```bash
# For Dart projects
dart pub add ack

# For Flutter projects
flutter pub add ack
```

Or add to your `pubspec.yaml` (check [pub.dev](https://pub.dev/packages/ack) for the latest version):

```yaml
dependencies:
  ack: ^1.0.0 # Replace with latest version
```

## Code generator (`@AckType()`)

To generate typed wrappers for hand-written schemas, add the annotation and generator packages alongside `ack`:

```bash
dart pub add ack ack_annotations
dart pub add --dev ack_generator build_runner
```

Or add them to your `pubspec.yaml`:

```yaml
dependencies:
  ack: ^1.0.0 # Replace with latest version
  ack_annotations: ^1.0.0 # Replace with latest version

dev_dependencies:
  ack_generator: ^1.0.0 # Replace with latest version
  build_runner: ^2.4.0
```

`ack_generator` does not generate schemas from classes. It reads top-level Ack schema variables and getters annotated with `@AckType()` and emits typed extension wrappers with `parse()`/`safeParse()` helpers.

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

part 'user.g.dart';

@AckType()
final userSchema = Ack.object({
  'name': Ack.string(),
  'email': Ack.string().email(),
});
```

Run the generator:

```bash
dart run build_runner build
```

See [TypeSafe Schemas](../core-concepts/typesafe-schemas.mdx) for more `@AckType` examples and supported schema shapes.

## Next step

Import `package:ack/ack.dart` and you're ready — the [Quickstart Tutorial](./quickstart-tutorial.mdx) takes you from your first schema to handling every validation outcome.

## Requirements

- Dart SDK: `>=3.8.0 <4.0.0`
```

### Quickstart Tutorial

Source: https://docs.page/conceptadev/ack/getting-started/quickstart-tutorial

```mdx

This tutorial validates a Dart map — the kind you get from `jsonDecode()` of an API response or a form submission — and handles every outcome.

## Prerequisites

[Install Ack](./installation.mdx) in your Dart or Flutter project.

## 1. Define a schema

Describe the shape you expect. Fields are required unless you mark them `.optional()`.

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

final userSchema = Ack.object({
  'name': Ack.string().minLength(2),      // required, min 2 chars
  'age': Ack.integer().min(0).optional(), // optional, non-negative
});
```

See [Schema Types](../core-concepts/schemas.mdx) and [Validation Rules](../core-concepts/validation.mdx) for everything you can express.

## 2. Validate data

Call `safeParse()` with the data you received — for example
`jsonDecode(response.body)`. It returns a `SchemaResult`, so expected validation
failures stay in normal control flow instead of throwing. For callback,
transform, and codec failure semantics, see [Error Handling](../core-concepts/error-handling.mdx).

```dart
final result = userSchema.safeParse({'name': 'Alice', 'age': 30});

if (result.isOk) {
  final user = result.getOrThrow();       // validated Map<String, Object?>
  print('Valid: $user');
} else {
  print('Invalid: ${result.getError()}'); // structured error with a path
}
```

Prefer exceptions? Use `parse()` instead — it returns the value or throws `AckException`.

## 3. Handle every outcome

A schema accepts valid data and rejects each way it can be wrong. This runnable example checks four inputs:

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

void main() {
  final userSchema = Ack.object({
    'name': Ack.string().minLength(2),
    'age': Ack.integer().min(0).optional(),
  });

  checkResult(userSchema.safeParse({'name': 'Alice', 'age': 30})); // OK
  checkResult(userSchema.safeParse({'name': 'Bob'}));              // OK: age omitted
  checkResult(userSchema.safeParse({'name': 'X', 'age': 25}));     // name too short
  checkResult(userSchema.safeParse({'age': 40}));                  // name missing
}

void checkResult(SchemaResult result) {
  if (result.isOk) {
    print('OK:     ${result.getOrThrow()}');
  } else {
    final error = result.getError();
    print('FAILED at ${error.path}: ${error.message}'); // path is a JSON Pointer
  }
}
```

See [Error Handling](../core-concepts/error-handling.mdx) to read and display errors.

## Next steps

- [Schema Types](../core-concepts/schemas.mdx) — all available schema types
- [Validation Rules](../core-concepts/validation.mdx) — built-in constraints
- [Custom Validation](../guides/custom-validation.mdx) — add your own logic
- [JSON Serialization](../core-concepts/json-serialization.mdx) — parse and encode JSON
```

### Common Recipes

Source: https://docs.page/conceptadev/ack/guides/common-recipes

```mdx

Quick solutions to common validation scenarios. Each recipe shows a working
schema pattern you can copy and adapt. Examples assume
`package:ack/ack.dart` is imported unless the snippet shows other imports.

## Email and password validation

Validate user credentials with proper constraints:

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

// Email with proper format
final emailSchema = Ack.string()
  .email()
  .notEmpty();

// Password with security requirements
final passwordSchema = Ack.string()
  .minLength(8)
  .matches(r'.*[A-Z].*', message: 'Password must contain an uppercase letter')
  .matches(r'.*[a-z].*', message: 'Password must contain a lowercase letter')
  .matches(r'.*[0-9].*', message: 'Password must contain a number');

// Login form schema
final loginSchema = Ack.object({
  'email': emailSchema,
  'password': passwordSchema,
});

// Usage
final result = loginSchema.safeParse({
  'email': 'user@example.com',
  'password': 'SecurePass123',
});

if (result.isOk) {
  print('Credentials valid');
} else {
  print('Error: ${result.getError()}');
}
```

## Nested object validation

Validate nested data structures like addresses:

```dart
final addressSchema = Ack.object({
  'street': Ack.string().notEmpty(),
  'city': Ack.string().notEmpty(),
  'zipCode': Ack.string().matches(r'^\d{5}(-\d{4})?$'),
  'country': Ack.string().notEmpty(),
});

final userWithAddressSchema = Ack.object({
  'name': Ack.string(),
  'email': Ack.string().email(),
  'shippingAddress': addressSchema,
  'billingAddress': addressSchema.optional().nullable(),
});

// Usage
final result = userWithAddressSchema.safeParse({
  'name': 'John Doe',
  'email': 'john@example.com',
  'shippingAddress': {
    'street': '123 Main St',
    'city': 'Springfield',
    'zipCode': '12345',
    'country': 'USA',
  },
});
```

## List validation

Validate lists with constraints on items and length:

```dart
// Shopping cart with at least 1 item
final cartSchema = Ack.object({
  'userId': Ack.string(),
  'items': Ack.list(Ack.object({
    'productId': Ack.string(),
    'quantity': Ack.integer().positive(),
    'price': Ack.double().positive(),
  })).minLength(1).maxLength(50),
});

// Tags with max 5 items
final postSchema = Ack.object({
  'title': Ack.string().minLength(5).maxLength(100),
  'content': Ack.string().minLength(10),
  'tags': Ack.list(Ack.string()).maxLength(5),
});
```

## Enum validation

Validate values against a set of allowed options using Dart enums:

```dart
enum OrderStatus { pending, processing, shipped, delivered, cancelled }
enum Priority { low, medium, high }

final orderSchema = Ack.object({
  'orderId': Ack.string(),
  'status': Ack.enumValues(OrderStatus.values),
  'priority': Ack.enumValues(Priority.values),
});

// Usage
final result = orderSchema.safeParse({
  'orderId': 'ORD-123',
  'status': 'shipped',
  'priority': 'high',
});
```

## Custom validation

Create reusable custom validators for domain-specific rules:

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

// Custom phone number constraint
class PhoneNumberConstraint extends Constraint<String> with Validator<String> {
  PhoneNumberConstraint()
      : super(
          constraintKey: 'phone_number',
          description: 'Must be valid phone number (e.g., +1-234-567-8900)',
        );

  final _regex = RegExp(r'^\+\d{1,3}-\d{3}-\d{3}-\d{4}$');

  @override
  bool isValid(String value) => _regex.hasMatch(value);

  @override
  String buildMessage(String value) =>
      'Must be valid phone number (e.g., +1-234-567-8900)';
}

final registrationSchema = Ack.object({
  'username': Ack.string().minLength(3),
  'email': Ack.string().email(),
  'phone': Ack.string().constrain(PhoneNumberConstraint()),
  'password': Ack.string().minLength(8),
  'confirmPassword': Ack.string().minLength(8),
}).refine(
  (data) => data['password'] == data['confirmPassword'],
  message: 'Passwords do not match',
);
```

## API response validation

Validate external API responses before using the payload. APIs commonly add
fields over time, so this boundary schema validates the fields the application
uses while allowing the rest of GitHub's response to pass through.

Add the HTTP client first with `dart pub add http`.

```dart
import 'dart:convert';

import 'package:ack/ack.dart';
import 'package:http/http.dart' as http;

// GitHub user API response
final githubUserSchema = Ack.object({
  'login': Ack.string(),
  'id': Ack.integer(),
  'avatar_url': Ack.string().url(),
  'name': Ack.string().nullable(),
  'email': Ack.string().email().nullable(),
  'bio': Ack.string().nullable(),
  'public_repos': Ack.integer(),
  'followers': Ack.integer(),
  'following': Ack.integer(),
  'created_at': Ack.string().datetime(),
}, additionalProperties: true);

// Usage in API call
Future<void> fetchUser(String username) async {
  final response = await http.get(
    Uri.parse('https://api.github.com/users/$username'),
  );

  if (response.statusCode != 200) {
    throw StateError('GitHub request failed: ${response.statusCode}');
  }

  final json = jsonDecode(response.body);
  final result = githubUserSchema.safeParse(json);

  if (result.isOk) {
    final user = result.getOrThrow()!;
    print('User: ${user['login']}');
  } else {
    print('Invalid API response: ${result.getError()}');
  }
}
```

## See also

- [Custom validation guide](./custom-validation.mdx) — deep dive into custom validators
- [Flutter form validation](./flutter-form-validation.mdx) — integrate with Flutter forms
- [Codecs](../core-concepts/codecs.mdx) — date, URI, and custom value conversions
- [JSON Schema integration](./json-schema-integration.mdx) — export schemas for API docs and tools
- [Schema types](../core-concepts/schemas.mdx) — all available schema types
- [Validation rules](../core-concepts/validation.mdx) — complete constraint reference
```

### Creating Adapter Packages

Source: https://docs.page/conceptadev/ack/guides/creating-schema-converter-packages

```mdx

This guide provides detailed instructions for creating schema converter packages that transform Ack validation schemas into other schema formats (e.g., JSON Schema, OpenAPI, GraphQL, Protobuf, TypeBox, AJV, etc.).

**Based on**: `ack_firebase_ai` package (reference implementation)

## Table of Contents

1. [Overview](#overview)
2. [Package Structure](#package-structure)
3. [Step-by-Step Implementation Guide](#step-by-step-implementation-guide)
4. [Architecture Patterns](#architecture-patterns)
5. [Testing Strategy](#testing-strategy)
6. [Documentation Requirements](#documentation-requirements)
7. [Common Patterns](#common-patterns)
8. [Examples](#examples)

---

## Overview

### Purpose

Schema converter packages bridge Ack's validation schemas with external schema systems, enabling:
- **Structured AI output** (Firebase AI, OpenAI Function Calling)
- **API documentation** (OpenAPI, GraphQL)
- **Cross-language validation** (JSON Schema, Protobuf)
- **Frontend validation** (TypeBox, Zod, Yup)

Current converter packages should use Ack's canonical export surface:

```dart
final model = schema.toSchemaModel();
final jsonSchema = schema.toJsonSchema();
```

`AckSchemaModel` describes the boundary shape, constraints, export-safe
defaults, discriminator metadata, and warnings that adapters can reuse for
non-JSON targets. JSON-map adapters can call `schema.toJsonSchema()` directly.
Adapters should not traverse `AckSchema` subclasses or parse rendered JSON
Schema output as their source of truth for non-JSON formats.

### Package Naming Convention

```
ack_<target_system>
```

**Examples**:
- `ack_firebase_ai` - Firebase AI (Gemini) schemas
- `ack_openapi` - OpenAPI 3.0/3.1 schemas
- `ack_graphql` - GraphQL SDL schemas
- `ack_protobuf` - Protocol Buffer schemas
- `ack_typebox` - TypeBox schemas
- `ack_ajv` - AJV JSON Schema schemas

---

## Package Structure

### Directory Layout

```
packages/ack_<target>/
├── lib/
│   ├── ack_<target>.dart           # Main library file (public API)
│   └── src/
│       ├── converter.dart          # Core conversion logic
│       └── extension.dart          # Extension method on AckSchema
├── test/
│   └── to_<target>_schema_test.dart  # Comprehensive test suite
├── example/
│   └── basic_usage.dart            # Usage examples
├── docs/
│   ├── <target>_schema_format.md   # Target schema documentation
│   └── migration_guide.md          # Migration/upgrade guide (if needed)
├── pubspec.yaml                    # Package metadata
├── README.md                       # User-facing documentation
├── CHANGELOG.md                    # Version history
├── LICENSE                         # License file
├── analysis_options.yaml           # Dart analyzer config
└── .pubignore                      # Publish exclusions
```

### File Responsibilities

| File | Purpose | Required? |
|------|---------|-----------|
| `lib/ack_<target>.dart` | Main entry point, exports public API | ✅ Yes |
| `lib/src/converter.dart` | Conversion logic (private) | ✅ Yes |
| `lib/src/extension.dart` | Extension methods (private) | ✅ Yes |
| `test/to_<target>_schema_test.dart` | Comprehensive tests | ✅ Yes |
| `example/basic_usage.dart` | Working examples | ✅ Yes |
| `README.md` | Documentation | ✅ Yes |
| `CHANGELOG.md` | Version history | ✅ Yes |
| `docs/` | Additional documentation | ⚠️ Recommended |

---

## Step-by-Step Implementation Guide

### Phase 1: Setup (30 minutes)

#### 1.1 Create Package Structure

```bash
cd packages/
mkdir ack_<target>
cd ack_<target>

# Create directories
mkdir -p lib/src test example docs

# Create files
touch lib/ack_<target>.dart
touch lib/src/converter.dart
touch lib/src/extension.dart
touch test/to_<target>_schema_test.dart
touch example/basic_usage.dart
touch README.md
touch CHANGELOG.md
touch pubspec.yaml
touch analysis_options.yaml
touch .pubignore
```

#### 1.2 Configure pubspec.yaml

```yaml
name: ack_<target>
description: <Target System> schema converter for Ack validation library
version: 1.0.0-beta.1
repository: https://github.com/btwld/ack
issue_tracker: https://github.com/btwld/ack/issues

environment:
  sdk: '>=3.8.0 <4.0.0'
  # Add flutter if target SDK requires it
  # flutter: '>=3.16.0'

dependencies:
  ack: ^1.0.0
  # Add target SDK dependency if needed
  # <target_sdk>: ^x.y.z
  meta: ^1.15.0

dev_dependencies:
  test: ^1.24.0
  lints: ^5.0.0
  # flutter_test:  # Only if using Flutter
  #   sdk: flutter
```

**Key decisions**:
- Does the target SDK require Flutter? (e.g., Firebase AI does)
- What's the minimum Dart SDK version?
- What version constraints for the target SDK?

#### 1.3 Configure analysis_options.yaml

```yaml
include: package:lints/recommended.yaml

analyzer:
  exclude:
    - "**/*.g.dart"
  language:
    strict-casts: true
    strict-inference: true
    strict-raw-types: true
```

#### 1.4 Configure .pubignore

```
# Development and IDE files
.claude/
.idea/
.vscode/

# Build artifacts
build/
.dart_tool/

# Coverage files
coverage/

# Local example files
example/local/
```

---

### Phase 2: Core Implementation (2-4 hours)

#### 2.1 Main Library File (`lib/ack_<target>.dart`)

**Template**:

```dart
/// <Target System> schema converter for Ack validation library.
///
/// Converts Ack validation schemas to <Target> format for [use case].
///
/// ## Usage
///
/// ```dart
/// import 'package:ack/ack.dart';
/// import 'package:ack_<target>/ack_<target>.dart';
///
/// final schema = Ack.object({
///   'name': Ack.string().minLength(2),
///   'age': Ack.integer().min(0).optional(),
/// });
///
/// // Convert to <Target> format
/// final targetSchema = schema.to<Target>Schema();
/// ```
///
/// ## Limitations
///
/// Some Ack features cannot be converted to <Target> format:
/// - [List specific limitations based on target system]
/// - Custom refinements (`.refine()`) - validate after
/// - [Other limitations...]
library;

import 'package:ack/ack.dart';
// Import target SDK if applicable
// import 'package:<target_sdk>/<target_sdk>.dart' as target;

// Export public API
export 'src/extension.dart';

// Optionally export converter if users need direct access
// export 'src/converter.dart' show <Target>SchemaConverter;
```

**Key sections**:
1. **Library documentation** - Top-level overview
2. **Usage example** - Quick start code
3. **Limitations** - What doesn't convert
4. **Exports** - Only export public API

#### 2.2 Extension Method (`lib/src/extension.dart`)

**Template**:

```dart
import 'package:ack/ack.dart';
// Import target type
// import 'package:<target_sdk>/<target_sdk>.dart' show <TargetSchema>;

import 'converter.dart';

/// Extension methods for converting Ack schemas to <Target> format.
extension <Target>SchemaExtension on AckSchema {
  /// Converts this Ack schema to <Target> format.
  ///
  /// Returns a [<TargetSchema>] instance that can be used with
  /// [describe target use case].
  ///
  /// ## Example
  ///
  /// ```dart
  /// final schema = Ack.object({
  ///   'name': Ack.string().minLength(2),
  ///   'age': Ack.integer().min(0).optional(),
  /// });
  ///
  /// final targetSchema = schema.to<Target>Schema();
  /// ```
  ///
  /// ## Limitations
  ///
  /// Some Ack features cannot be converted:
  /// - [List specific limitations]
  /// - Custom refinements (`.refine()`)
  /// - Regex patterns (`.matches()`)
  /// - [Other limitations...]
  ///
  /// ## <Target> Schema Format
  ///
  /// The returned [<TargetSchema>] follows <Target>'s schema format.
  /// Key fields include:
  /// - [Describe key schema fields]
  /// - [Describe structure]
  <TargetSchema> to<Target>Schema() {
    return <Target>SchemaConverter.convert(this);
  }
}
```

**Design pattern**: Simple extension that delegates to converter

#### 2.3 Converter Logic (`lib/src/converter.dart`)

**Template**:

```dart
import 'package:ack/ack.dart';
// Import target SDK
// import 'package:<target_sdk>/<target_sdk>.dart' as target;

/// Converts Ack schemas to <Target> format.
///
/// <Target> uses [describe schema format] for [describe use case].
///
/// This is a utility class with only static methods and cannot be instantiated.
class <Target>SchemaConverter {
  // Private constructor prevents instantiation
  const <Target>SchemaConverter._();

  /// Converts an Ack schema to <Target> format.
  ///
  /// Returns a [<TargetSchema>] representing the schema structure.
  static <TargetSchema> convert(AckSchema schema) {
    return _convertModel(schema.toSchemaModel());
  }

  static <TargetSchema> _convertModel(AckSchemaModel schema) {
    return switch (schema) {
      AckStringSchemaModel() => _convertString(schema),
      AckIntegerSchemaModel() => _convertInteger(schema),
      AckNumberSchemaModel() => _convertNumber(schema),
      AckBooleanSchemaModel() => _convertBoolean(schema),
      AckObjectSchemaModel() => _convertObject(schema),
      AckArraySchemaModel() => _convertArray(schema),
      AckAnyOfSchemaModel() => _convertAnyOf(schema),
      AckOneOfSchemaModel() => _convertOneOf(schema),
      AckAllOfSchemaModel() => _convertAllOf(schema),
      AckNullSchemaModel() => _buildNullSchema(),
      // Lazy/recursive schemas (Ack.lazy) surface as references by name.
      // Map schema.refName to your target's reference mechanism, or throw if
      // the target cannot express recursion.
      AckRefSchemaModel() => throw UnsupportedError(
        'Reference schemas (Ack.lazy) are not supported by this target.',
      ),
    };
  }

  // ========================================================================
  // Primitive Type Converters
  // ========================================================================

  static <TargetSchema> _convertString(AckStringSchemaModel schema) {
    final enumValues = schema.allowedStringValues;
    if (enumValues != null) {
      return _buildEnumSchema(enumValues, schema);
    }

    return _buildStringSchema(
      description: schema.description,
      nullable: schema.nullable,
      format: schema.format,
      minLength: schema.minLength,
      maxLength: schema.maxLength,
      pattern: schema.pattern,
    );
  }

  static <TargetSchema> _convertInteger(AckIntegerSchemaModel schema) {
    return _buildIntegerSchema(
      description: schema.description,
      nullable: schema.nullable,
      minimum: schema.minimum,
      maximum: schema.maximum,
      exclusiveMinimum: schema.exclusiveMinimum,
      exclusiveMaximum: schema.exclusiveMaximum,
    );
  }

  static <TargetSchema> _convertNumber(AckNumberSchemaModel schema) {
    return _buildNumberSchema(
      description: schema.description,
      nullable: schema.nullable,
      minimum: schema.minimum,
      maximum: schema.maximum,
    );
  }

  static <TargetSchema> _convertBoolean(AckBooleanSchemaModel schema) {
    return _buildBooleanSchema(
      description: schema.description,
      nullable: schema.nullable,
    );
  }

  // ========================================================================
  // Complex Type Converters
  // ========================================================================

  static <TargetSchema> _convertObject(AckObjectSchemaModel schema) {
    final properties = <String, TargetSchema>{};
    for (final entry in schema.properties?.entries ?? const []) {
      properties[entry.key] = _convertModel(entry.value);
    }

    final required = schema.required ?? const [];
    final optionalProperties = properties.keys
        .where((key) => !required.contains(key))
        .toList();

    final additionalProperties = switch (schema.additionalProperties) {
      AckAdditionalPropertiesAllowed() || null => true,
      AckAdditionalPropertiesDisallowed() => false,
      AckAdditionalPropertiesSchema(schema: final schema) =>
        _convertModel(schema),
    };

    return _buildObjectSchema(
      properties: properties,
      optionalProperties: optionalProperties,
      description: schema.description,
      nullable: schema.nullable,
      additionalProperties: additionalProperties,
    );
  }

  static <TargetSchema> _convertArray(AckArraySchemaModel schema) {
    final itemSchema = schema.items != null
        ? _convertModel(schema.items!)
        : _buildAnySchema();

    return _buildArraySchema(
      items: itemSchema,
      description: schema.description,
      nullable: schema.nullable,
      minItems: schema.minItems,
      maxItems: schema.maxItems,
    );
  }

  static <TargetSchema> _convertAnyOf(AckAnyOfSchemaModel schema) {
    final branches = schema.schemas.map(_convertModel).toList();

    return _buildAnyOfSchema(
      branches: branches,
      description: schema.description,
      nullable: schema.nullable,
      discriminator: schema.discriminator?.propertyName,
    );
  }

  static <TargetSchema> _convertOneOf(AckOneOfSchemaModel schema) {
    final branches = schema.schemas.map(_convertModel).toList();

    return _buildOneOfSchema(
      branches: branches,
      description: schema.description,
      nullable: schema.nullable,
    );
  }

  static <TargetSchema> _convertAllOf(AckAllOfSchemaModel schema) {
    final branches = schema.schemas.map(_convertModel).toList();

    return _buildAllOfSchema(
      branches: branches,
      description: schema.description,
      nullable: schema.nullable,
    );
  }

  // ========================================================================
  // Helper Methods - Schema Builders
  // ========================================================================
  // These wrap the target SDK's schema construction API

  static <TargetSchema> _buildStringSchema({
    String? description,
    bool nullable = false,
    String? format,
    int? minLength,
    int? maxLength,
    String? pattern,
  }) {
    return <String, Object?>{
      'type': 'string',
      if (description != null) 'description': description,
      if (nullable) 'nullable': true,
      if (format != null) 'format': format,
      if (minLength != null) 'minLength': minLength,
      if (maxLength != null) 'maxLength': maxLength,
      if (pattern != null) 'pattern': pattern,
    } as TargetSchema;
  }

  static <TargetSchema> _buildIntegerSchema({
    String? description,
    bool nullable = false,
    num? minimum,
    num? maximum,
    num? exclusiveMinimum,
    num? exclusiveMaximum,
  }) {
    return <String, Object?>{
      'type': 'integer',
      if (description != null) 'description': description,
      if (nullable) 'nullable': true,
      if (minimum != null) 'minimum': minimum,
      if (maximum != null) 'maximum': maximum,
      if (exclusiveMinimum != null)
        'exclusiveMinimum': exclusiveMinimum,
      if (exclusiveMaximum != null)
        'exclusiveMaximum': exclusiveMaximum,
    } as TargetSchema;
  }

  static <TargetSchema> _buildNumberSchema({
    String? description,
    bool nullable = false,
    num? minimum,
    num? maximum,
  }) {
    return <String, Object?>{
      'type': 'number',
      if (description != null) 'description': description,
      if (nullable) 'nullable': true,
      if (minimum != null) 'minimum': minimum,
      if (maximum != null) 'maximum': maximum,
    } as TargetSchema;
  }

  static <TargetSchema> _buildBooleanSchema({
    String? description,
    bool nullable = false,
  }) {
    return <String, Object?>{
      'type': 'boolean',
      if (description != null) 'description': description,
      if (nullable) 'nullable': true,
    } as TargetSchema;
  }

  static <TargetSchema> _buildObjectSchema({
    required Map<String, TargetSchema> properties,
    List<String>? optionalProperties,
    String? description,
    bool nullable = false,
    Object additionalProperties = false,
  }) {
    final required = <String>[];
    for (final propertyName in properties.keys) {
      if (optionalProperties == null || !optionalProperties.contains(propertyName)) {
        required.add(propertyName);
      }
    }

    return <String, Object?>{
      'type': 'object',
      'properties': properties,
      if (required.isNotEmpty) 'required': required,
      if (description != null) 'description': description,
      if (nullable) 'nullable': true,
      'additionalProperties': additionalProperties,
    } as TargetSchema;
  }

  static <TargetSchema> _buildArraySchema({
    required TargetSchema items,
    String? description,
    bool nullable = false,
    int? minItems,
    int? maxItems,
  }) {
    return <String, Object?>{
      'type': 'array',
      'items': items,
      if (description != null) 'description': description,
      if (nullable) 'nullable': true,
      if (minItems != null) 'minItems': minItems,
      if (maxItems != null) 'maxItems': maxItems,
    } as TargetSchema;
  }

  static <TargetSchema> _buildEnumSchema(
    List<Object?> enumValues,
    AckSchemaModel schema,
  ) {
    return <String, Object?>{
      'type': 'string',
      'enum': enumValues.map((value) => value.toString()).toList(),
      if (schema.nullable) 'nullable': true,
      if (schema.description != null) 'description': schema.description,
    } as TargetSchema;
  }

  static <TargetSchema> _buildAnyOfSchema({
    required List<TargetSchema> branches,
    String? description,
    bool nullable = false,
    String? discriminator,
  }) {
    return <String, Object?>{
      'type': 'anyOf',
      'branches': branches,
      if (description != null) 'description': description,
      if (nullable) 'nullable': true,
      if (discriminator != null) 'discriminator': discriminator,
    } as TargetSchema;
  }

  static <TargetSchema> _buildOneOfSchema({
    required List<TargetSchema> branches,
    String? description,
    bool nullable = false,
  }) {
    return <String, Object?>{
      'type': 'oneOf',
      'branches': branches,
      if (description != null) 'description': description,
      if (nullable) 'nullable': true,
    } as TargetSchema;
  }

  static <TargetSchema> _buildAllOfSchema({
    required List<TargetSchema> branches,
    String? description,
    bool nullable = false,
  }) {
    return <String, Object?>{
      'type': 'allOf',
      'branches': branches,
      if (description != null) 'description': description,
      if (nullable) 'nullable': true,
    } as TargetSchema;
  }

  // Add target SDK coercion helpers here only when the target builder API
  // needs a narrower type than AckSchemaModel exposes.
}
```

**Key patterns**:
1. **Private constructor** - Prevent instantiation
2. **Static converter methods** - Pure functions
3. **Model-first routing** - Convert once with `schema.toSchemaModel()`
4. **Switch expression** - Type-safe routing on sealed `AckSchemaModel` variants
5. **Helper builders** - Wrap target SDK API
6. **Explicit gaps** - Throw or warn for target-unsupported model features

---

## Testing Strategy

### Phase 3: Testing (2-3 hours)

#### 3.1 Test Structure

**Template** (`test/to_<target>_schema_test.dart`):

```dart
import 'package:ack/ack.dart';
import 'package:ack_<target>/ack_<target>.dart';
// Import target SDK for assertions
// import 'package:<target_sdk>/<target_sdk>.dart' as target;
import 'package:test/test.dart';

// Test data
enum Color { red, green, blue }
enum Status { pending, active, completed }

/// Tests for the to<Target>Schema() extension method.
///
/// Coverage areas:
/// - Basic schema conversion (primitives, objects, arrays)
/// - Constraint mapping
/// - Edge cases and error handling
/// - Semantic validation (behavioral equivalence)
/// - Metadata and descriptions
/// - Dart enum support
void main() {
  group('to<Target>Schema()', () {
    group('Primitives', () {
      test('converts basic string schema', () {
        final schema = Ack.string();
        final result = schema.to<Target>Schema();

        // Assert target schema properties
        expect(result.type, target.SchemaType.string);
        expect(result.nullable, isFalse);
      });

      test('converts string with description', () {
        final schema = Ack.string().describe('User name');
        final result = schema.to<Target>Schema();

        expect(result.description, 'User name');
      });

      test('converts nullable string', () {
        final schema = Ack.string().nullable();
        final result = schema.to<Target>Schema();

        expect(result.nullable, isTrue);
      });

      test('converts string with minLength', () {
        final schema = Ack.string().minLength(5);
        final result = schema.to<Target>Schema();

        // Assert minLength is preserved (if supported by target)
        expect(result.minLength, 5);
      });

      test('converts string with maxLength', () {
        final schema = Ack.string().maxLength(50);
        final result = schema.to<Target>Schema();

        expect(result.maxLength, 50);
      });

      test('converts string with email format', () {
        final schema = Ack.string().email();
        final result = schema.to<Target>Schema();

        expect(result.format, 'email');
      });

      test('converts integer schema', () {
        final schema = Ack.integer();
        final result = schema.to<Target>Schema();

        expect(result.type, target.SchemaType.integer);
      });

      test('converts integer with minimum', () {
        final schema = Ack.integer().min(0);
        final result = schema.to<Target>Schema();

        expect(result.minimum, 0);
      });

      test('converts integer with maximum', () {
        final schema = Ack.integer().max(100);
        final result = schema.to<Target>Schema();

        expect(result.maximum, 100);
      });

      test('converts double schema', () {
        final schema = Ack.double();
        final result = schema.to<Target>Schema();

        expect(result.type, target.SchemaType.number);
      });

      test('converts double with range', () {
        final schema = Ack.double().min(0.0).max(1.0);
        final result = schema.to<Target>Schema();

        expect(result.minimum, 0.0);
        expect(result.maximum, 1.0);
      });

      test('converts boolean schema', () {
        final schema = Ack.boolean();
        final result = schema.to<Target>Schema();

        expect(result.type, target.SchemaType.boolean);
      });
    });

    group('Objects', () {
      test('converts basic object schema', () {
        final schema = Ack.object({
          'name': Ack.string(),
          'age': Ack.integer(),
        });
        final result = schema.to<Target>Schema();

        expect(result.type, target.SchemaType.object);
        expect(result.properties.keys, containsAll(['name', 'age']));
      });

      test('converts object with optional fields', () {
        final schema = Ack.object({
          'name': Ack.string(),
          'age': Ack.integer().optional(),
        });
        final result = schema.to<Target>Schema();

        expect(result.required, contains('name'));
        expect(result.required, isNot(contains('age')));
        // OR: expect(result.optionalProperties, contains('age'));
      });

      test('converts nested object schema', () {
        final schema = Ack.object({
          'user': Ack.object({
            'name': Ack.string(),
          }),
        });
        final result = schema.to<Target>Schema();

        expect(result.properties['user']?.type, target.SchemaType.object);
      });

      test('uses model property ordering if the target supports it', () {
        final schema = Ack.object({
          'id': Ack.string(),
          'name': Ack.string(),
          'email': Ack.string(),
        });
        final result = schema.to<Target>Schema();

        // This comes from AckObjectSchemaModel metadata, not generic JSON Schema.
        expect(result.propertyOrdering, ['id', 'name', 'email']);
      });
    });

    group('Arrays', () {
      test('converts basic array schema', () {
        final schema = Ack.list(Ack.string());
        final result = schema.to<Target>Schema();

        expect(result.type, target.SchemaType.array);
        expect(result.items.type, target.SchemaType.string);
      });

      test('converts array with minItems', () {
        final schema = Ack.list(Ack.string()).minLength(1);
        final result = schema.to<Target>Schema();

        expect(result.minItems, 1);
      });

      test('converts array with maxItems', () {
        final schema = Ack.list(Ack.string()).maxLength(10);
        final result = schema.to<Target>Schema();

        expect(result.maxItems, 10);
      });

      test('converts array of objects', () {
        final schema = Ack.list(
          Ack.object({
            'id': Ack.integer(),
            'name': Ack.string(),
          }),
        );
        final result = schema.to<Target>Schema();

        expect(result.items.type, target.SchemaType.object);
      });
    });

    group('Enums', () {
      test('converts string enum schema', () {
        final schema = Ack.enumString(['red', 'green', 'blue']);
        final result = schema.to<Target>Schema();

        expect(result.enumValues, ['red', 'green', 'blue']);
      });

      test('converts Dart enum to string enumValues', () {
        final schema = EnumSchema<Color>(values: Color.values);
        final result = schema.to<Target>Schema();

        expect(result.enumValues, ['red', 'green', 'blue']);
      });
    });

    group('Edge Cases', () {
      test('handles anyOf schema', () {
        final schema = Ack.anyOf([
          Ack.string(),
          Ack.integer(),
        ]);
        final result = schema.to<Target>Schema();

        expect(result.anyOf, hasLength(2));
      });

      test('throws UnsupportedError for unsupported schema types', () {
        final schema = Ack.string().refine((s) => s.startsWith('A'));

        // Refinements should not be supported
        // Check if it throws or handles gracefully
        expect(
          () => schema.to<Target>Schema(),
          throwsUnsupportedError,
        );
      });

      test('handles empty object schema', () {
        final schema = Ack.object({});
        final result = schema.to<Target>Schema();

        expect(result.type, target.SchemaType.object);
        expect(result.properties, isEmpty);
      });
    });

    group('Metadata', () {
      test('preserves description metadata', () {
        final schema = Ack.string().describe('User email address');
        final result = schema.to<Target>Schema();

        expect(result.description, 'User email address');
      });

      test('preserves nullable flag', () {
        final schema = Ack.string().nullable();
        final result = schema.to<Target>Schema();

        expect(result.nullable, isTrue);
      });

      test('handles title metadata if supported', () {
        final schemaWithTitle = Ack.string(); // Add title somehow
        final result = schemaWithTitle.to<Target>Schema();

        // Check if title is preserved
        // expect(result.title, 'Some Title');
      });
    });

    group('Semantic Validation', () {
      // Test that converted schemas behave correctly with target system

      test('validates string constraints correctly', () {
        final schema = Ack.string().minLength(5).maxLength(10);
        final targetSchema = schema.to<Target>Schema();

        // Test with target system's validator
        final validResult = targetSchema.validate('hello');
        final invalidShort = targetSchema.validate('hi');
        final invalidLong = targetSchema.validate('this is too long');

        expect(validResult.isValid, isTrue);
        expect(invalidShort.isValid, isFalse);
        expect(invalidLong.isValid, isFalse);
      });

      test('validates integer range correctly', () {
        final schema = Ack.integer().min(0).max(100);
        final targetSchema = schema.to<Target>Schema();

        expect(targetSchema.validate(50).isValid, isTrue);
        expect(targetSchema.validate(-1).isValid, isFalse);
        expect(targetSchema.validate(101).isValid, isFalse);
      });

      test('validates enum values correctly', () {
        final schema = Ack.enumString(['red', 'green', 'blue']);
        final targetSchema = schema.to<Target>Schema();

        expect(targetSchema.validate('red').isValid, isTrue);
        expect(targetSchema.validate('yellow').isValid, isFalse);
      });
    });

    group('Complex Scenarios', () {
      test('converts complete nested structure', () {
        final schema = Ack.object({
          'user': Ack.object({
            'id': Ack.integer().min(1),
            'name': Ack.string().minLength(2),
            'email': Ack.string().email(),
            'roles': Ack.list(Ack.string()),
            'metadata': Ack.object({
              'createdAt': Ack.string(),
              'updatedAt': Ack.string().optional(),
            }),
          }),
          'tags': Ack.list(Ack.string()).optional(),
        });

        final result = schema.to<Target>Schema();

        // Verify structure
        expect(result.type, target.SchemaType.object);
        expect(result.properties.containsKey('user'), isTrue);
        expect(result.properties.containsKey('tags'), isTrue);

        final userSchema = result.properties['user']!;
        expect(userSchema.properties.containsKey('metadata'), isTrue);
      });
    });
  });
}
```

**Test categories**:
1. **Primitives** - Basic type conversions
2. **Objects** - Complex structures, nesting
3. **Arrays** - Lists with constraints
4. **Enums** - String and Dart enums
5. **Edge Cases** - Empty, null, unsupported
6. **Metadata** - Descriptions, titles
7. **Semantic Validation** - Actual behavior with target system
8. **Complex Scenarios** - Real-world structures

**Coverage target**: 85%+ line coverage

---

## Documentation Requirements

### Phase 4: Documentation (1-2 hours)

#### 4.1 README.md Template

```markdown
# ack_<target>

<Target System> schema converter for the [Ack](https://pub.dev/packages/ack) validation library.

[![pub package](https://img.shields.io/pub/v/ack_<target>.svg)](https://pub.dev/packages/ack_<target>)

## Overview

Converts Ack schemas to <Target> format for [use case]. Assumes familiarity with [Ack](https://pub.dev/packages/ack) and [target system].

## Installation

\`\`\`yaml
dependencies:
  ack: ^1.0.0
  ack_<target>: ^1.0.0
  <target_sdk>: ^x.y.z  # Required peer dependency
\`\`\`

### Compatibility

Requires `<target_sdk>: >=x.y.z <n.0.0` as a peer dependency. Report [compatibility issues](https://github.com/btwld/ack/issues).

## Limitations ⚠️

**Read this first** - <Target> schema conversion has important constraints:

### 1. [Primary Limitation]

[Explain the most important limitation]

**Example**:
\`\`\`dart
// What doesn't work and why
\`\`\`

### 2. [Secondary Limitation]

[Explain]

### 3. [Other Limitations]

- [List other limitations]
- [Feature gaps]
- [Workarounds]

## Usage

\`\`\`dart
import 'package:ack/ack.dart';
import 'package:ack_<target>/ack_<target>.dart';
import 'package:<target_sdk>/<target_sdk>.dart';

// 1. Define schema
final userSchema = Ack.object({
  'name': Ack.string().minLength(2).maxLength(50),
  'email': Ack.string().email(),
  'age': Ack.integer().min(0).max(120).optional(),
});

// 2. Convert to <Target>
final targetSchema = userSchema.to<Target>Schema();

// 3. Use with <Target> system
[Show actual usage with target system]

// 4. ALWAYS validate with Ack after
final result = userSchema.safeParse(responseData);
if (result.isOk) {
  final user = result.getOrThrow();
  print('Valid: $user');
} else {
  print('Invalid: ${result.getError()}');
}
\`\`\`

## Schema Mapping

### Supported Types

| Ack Type | <Target> Type | Notes |
|----------|---------------|-------|
| `Ack.string()` | [target type] | [Notes] |
| `Ack.integer()` | [target type] | [Notes] |
| `Ack.double()` | [target type] | [Notes] |
| `Ack.boolean()` | [target type] | [Notes] |
| `Ack.object({...})` | [target type] | [Notes] |
| `Ack.list(...)` | [target type] | [Notes] |
| `Ack.enumString([...])` | [target type] | [Notes] |
| `Ack.anyOf([...])` | [target type] | [Notes] |

### Supported Constraints

| Ack Constraint | <Target> | Notes |
|----------------|----------|-------|
| `.minLength()` / `.maxLength()` | [mapping] | [Notes] |
| `.min()` / `.max()` | [mapping] | [Notes] |
| `.email()` / `.uuid()` / `.url()` | [mapping] | [Notes] |
| `.nullable()` | [mapping] | [Notes] |
| `.optional()` | [mapping] | [Notes] |
| `.describe()` | [mapping] | [Notes] |

## Testing

[Instructions for running tests]

\`\`\`bash
cd packages/ack_<target>
dart test  # or flutter test if using Flutter
\`\`\`

## Contributing

For contribution guidelines, see the [CONTRIBUTING.md](https://github.com/btwld/ack/blob/main/CONTRIBUTING.md) in the root repository.

## License

This package is part of the [Ack](https://github.com/btwld/ack) monorepo.

## Related Packages

- [ack](https://pub.dev/packages/ack) - Core validation library
- [ack_generator](https://pub.dev/packages/ack_generator) - Code generator
- [<target_sdk>](https://pub.dev/packages/<target_sdk>) - <Target> SDK
```

#### 4.2 CHANGELOG.md Template

```markdown
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0-beta.1] - YYYY-MM-DD

### Added
- Initial release of ack_<target> package
- Extension method `.to<Target>Schema()` for converting Ack schemas
- Support for all basic schema types (string, integer, double, boolean, object, array)
- Support for enum schemas
- Constraint mapping ([list key constraints])
- Nullable and optional field support
- Comprehensive test suite with [X]+ tests
- Full documentation and examples

### Supported
- [List supported features]

### Limitations
- [List known limitations]

[1.0.0-beta.1]: https://github.com/btwld/ack/releases/tag/ack_<target>-v1.0.0-beta.1
```

---

## Architecture Patterns

### Pattern 1: AckSchemaModel Renderer

**When to use**: Default for new converter packages

**Implementation**:
```dart
extension TargetSchemaExtension on AckSchema {
  TargetSchema toTargetSchema() => _convert(toSchemaModel());
}

TargetSchema _convert(AckSchemaModel schema) {
  return switch (schema) {
    AckStringSchemaModel() => _convertString(schema),
    AckIntegerSchemaModel() => _convertInteger(schema),
    AckNumberSchemaModel() => _convertNumber(schema),
    AckBooleanSchemaModel() => _convertBoolean(schema),
    AckArraySchemaModel() => _convertArray(schema),
    AckObjectSchemaModel() => _convertObject(schema),
    AckAnyOfSchemaModel() => _convertAnyOf(schema),
    AckOneOfSchemaModel() => _convertOneOf(schema),
    AckAllOfSchemaModel() => _convertAllOf(schema),
    AckNullSchemaModel() => TargetSchema.nullValue(),
    AckRefSchemaModel() => TargetSchema.ref(schema.refName),
  };
}

TargetSchema _convertString(AckStringSchemaModel schema) {
  return TargetSchema.string(
    format: schema.format,
    minLength: schema.minLength,
    description: schema.description,
  );
}
```

**Pros**: One normalized Ack-owned model, shared semantics with maintained
adapter packages, no duplicated discriminator/default/nullability traversal
**Cons**: Target-specific unsupported features still need explicit handling

### Pattern 2: JSON Schema Renderer

**When to use**: Target system consumes JSON Schema-compatible maps directly

**Implementation**:
```dart
Map<String, Object?> _convert(AckSchema schema) {
  return schema.toJsonSchema();
}
```

**Pros**: Uses the canonical model while emitting a standard map format
**Cons**: Target systems that are not JSON Schema-compatible still need a model renderer

### Pattern 3: Target-Specific Model Post-Processing

**When to use**: Target systems require extra annotations after the canonical
model has been rendered

**Implementation**:
```dart
TargetSchema _convert(AckSchema schema) {
  final model = schema.toSchemaModel();
  final converted = _convertModel(model);
  return _applyTargetAnnotations(converted, model.extensions);
}
```

**Pros**: Keeps conversion anchored on `AckSchemaModel` while leaving room for target-specific metadata
**Cons**: Extra metadata must be documented and tested per target

---

## Common Patterns

### Handling Transformed Schemas

`toSchemaModel()` projects transformed schemas from their boundary input schema
and preserves adapter-safe metadata such as description, nullability, and
`x-transformed`.

**Option 1: Render projected boundary model** (Recommended)
```dart
TargetSchema _convertTransformed(AckSchemaModel schema) {
  return _convert(schema);
}
```

**Option 2: Reject explicit transformed marker** (If target cannot tolerate transforms)
```dart
if (schema.extensions['x-transformed'] == true) {
  throw UnsupportedError('Transformed Ack schemas are not supported here.');
}
```

### Handling Discriminated Unions

**Option 1: Native Support** (If target has discriminators)
```dart
static TargetSchema _convertDiscriminated(
  AckAnyOfSchemaModel schema,
) {
  return TargetSchema.discriminated(
    discriminatorKey: schema.discriminator!.propertyName,
    branches: [for (final branch in schema.schemas) _convert(branch)],
  );
}
```

**Option 2: AnyOf with Effective Discriminator Properties** (Fallback)
```dart
static TargetSchema _convertDiscriminated(
  AckAnyOfSchemaModel schema,
) {
  final branches = <TargetSchema>[];

  for (final branch in schema.schemas) {
    // Branches already expose the union-owned discriminator as an exact const.
    branches.add(_convert(branch));
  }

  return TargetSchema.anyOf(
    branches,
    discriminator: schema.discriminator?.propertyName,
  );
}
```

### Handling Any Schema Models

`Ack.any()` reaches converters as an `AckAnyOfSchemaModel` over the
JSON-compatible value shapes Ack can export. Inspect `schema.warnings` if the
target needs to surface that runtime `Object` acceptance is wider than the
export boundary.

**Option 1: Union of JSON-Compatible Shapes** (Canonical)
```dart
static TargetSchema _convertAny(AckAnyOfSchemaModel schema) {
  return TargetSchema.anyOf([
    for (final branch in schema.schemas) _convert(branch),
  ]);
}
```

**Option 2: Target-Specific Any** (If available)
```dart
static TargetSchema _convertAny(AckAnyOfSchemaModel schema) {
  return TargetSchema.any(
    description: schema.description,
    nullable: schema.nullable,
  );
}
```

**Option 3: Empty Object Fallback** (If the target has no union support)
```dart
static TargetSchema _convertAny(AckAnyOfSchemaModel schema) {
  return TargetSchema.object(
    properties: const {},
    additionalProperties: true,
    description: schema.description,
  );
}
```

### Target Builder Helpers

`AckSchemaModel` exposes typed fields for constraints and enum values, so
converter packages should not parse JSON Schema maps to recover them. Add helper
functions only for target SDK requirements, such as adapting `num` bounds to a
target API that requires `int` or `double`.

---

## Examples

### Example 1: OpenAPI Schema Converter

**Target**: OpenAPI 3.1 Schema

```dart
// lib/src/converter.dart
import 'package:ack/ack.dart';

class OpenApiSchemaConverter {
  const OpenApiSchemaConverter._();

  static Map<String, Object?> convert(AckSchema schema) {
    // OpenAPI 3.1 is compatible with the generic Draft-7 renderer for
    // schemas that do not need OpenAPI-specific extensions.
    return schema.toJsonSchema();
  }
}
```

### Example 2: GraphQL SDL Converter

**Target**: GraphQL Schema Definition Language

```dart
// lib/src/converter.dart
import 'package:ack/ack.dart';

class GraphQlSchemaConverter {
  const GraphQlSchemaConverter._();

  static String convert(AckSchema schema, {required String typeName}) {
    final buffer = StringBuffer();
    _convertToSDL(schema.toSchemaModel(), typeName, buffer);
    return buffer.toString();
  }

  static void _convertToSDL(
    AckSchemaModel schema,
    String typeName,
    StringBuffer buffer,
  ) {
    if (schema is! AckObjectSchemaModel) {
      throw UnsupportedError('Only object schema models can be converted.');
    }

    buffer.writeln('type $typeName {');

    final required = schema.required ?? const <String>[];
    for (final entry in schema.properties?.entries ?? const []) {
      final fieldName = entry.key;
      final fieldSchema = entry.value;
      final gqlType = _getGraphQLType(fieldSchema);
      final nullableSuffix = required.contains(fieldName) ? '!' : '';

      if (fieldSchema.description != null) {
        buffer.writeln('  """${fieldSchema.description}"""');
      }
      buffer.writeln('  $fieldName: $gqlType$nullableSuffix');
    }

    buffer.writeln('}');
  }

  static String _getGraphQLType(AckSchemaModel schema) {
    return switch (schema) {
      AckStringSchemaModel(allowedStringValues: final values)
          when values != null && values.isNotEmpty =>
        _generateEnumType(schema),
      AckStringSchemaModel() => 'String',
      AckIntegerSchemaModel() => 'Int',
      AckNumberSchemaModel() => 'Float',
      AckBooleanSchemaModel() => 'Boolean',
      AckArraySchemaModel(items: final item?) => '[${_getGraphQLType(item)}]',
      _ => 'String', // Fallback
    };
  }

  static String _generateEnumType(AckStringSchemaModel schema) {
    // Would need to generate enum definitions separately
    return 'EnumType';
  }
}
```

---

## Checklist

### Implementation Phase
- [ ] Create package directory structure
- [ ] Configure `pubspec.yaml` with correct dependencies
- [ ] Implement main library file with documentation
- [ ] Implement extension method
- [ ] Implement converter with all schema types
- [ ] Add type coercion helpers
- [ ] Handle edge cases (CodecSchema transforms, AnySchema, etc.)

### Testing Phase
- [ ] Write tests for all primitive types
- [ ] Write tests for complex types (object, array)
- [ ] Write tests for enums (string and Dart)
- [ ] Write tests for anyOf/discriminated unions
- [ ] Write tests for constraints and metadata
- [ ] Write tests for edge cases
- [ ] Add semantic validation tests
- [ ] Achieve 85%+ test coverage

### Documentation Phase
- [ ] Write comprehensive README
- [ ] Document all limitations upfront
- [ ] Add usage examples
- [ ] Create schema mapping tables
- [ ] Write CHANGELOG
- [ ] Add inline code documentation
- [ ] Create additional docs (if needed)

### Quality Assurance
- [ ] All tests pass
- [ ] `dart analyze` shows no issues
- [ ] `dart format` applied
- [ ] Examples run successfully
- [ ] README reviewed for clarity
- [ ] Limitations clearly documented

### Publication
- [ ] Version set correctly in pubspec.yaml
- [ ] CHANGELOG updated
- [ ] README finalized
- [ ] .pubignore configured
- [ ] Package published to pub.dev
- [ ] PR created for monorepo integration

---

## Additional Resources

### Reference Implementations
- **ack_firebase_ai**: Firebase AI/Gemini schemas
- **ack core**: JSON Schema implementation (`toJsonSchema()`)

### Target System Documentation
- Research target schema format documentation
- Understand supported types and constraints
- Identify gaps vs Ack features
- Document limitations clearly

### Monorepo Integration
- Add to `melos.yaml` packages list
- Configure CI/CD for testing
- Update root README with new package
- Add to documentation site

---

## Questions & Support

**Before starting**:
1. Does the target system have an official schema format?
2. Is there a Dart/Flutter SDK for the target?
3. What schema features does the target support?
4. What constraints can be represented?
5. How are nullability and optionality handled?

**During development**:
- Reference `ack_firebase_ai` for patterns
- Use `schema.toSchemaModel()` as the adapter boundary
- Write tests first (TDD approach)
- Document limitations as you discover them

**For help**:
- Create GitHub issue: https://github.com/btwld/ack/issues
- Reference this guide
- Ask specific questions about target system
```

### Custom Validation Rules

Source: https://docs.page/conceptadev/ack/guides/custom-validation

```mdx

While Ack provides many [built-in validation rules](../core-concepts/validation.mdx), you can extend them with your own value-level constraints or object-level refinement logic.

## Prerequisites

- **[Validation Rules](/core-concepts/validation)**: Built-in constraints and how they work
- **[Schema Types](/core-concepts/schemas)**: Different schema types and their behavior
- **[Error Handling](/core-concepts/error-handling)**: How validation errors are structured

## Creating a value constraint

To add reusable validation that only depends on the field value, implement a `Constraint<T>` that mixes in `Validator<T>`.

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

class IsPositiveConstraint extends Constraint<double> with Validator<double> {
  IsPositiveConstraint()
      : super(
          constraintKey: 'is_positive',
          description: 'Number must be positive',
        );

  @override
  bool isValid(double value) => value > 0;

  @override
  String buildMessage(double value) => 'Number must be positive';
}

final priceSchema = Ack.double().constrain(IsPositiveConstraint());

print(priceSchema.safeParse(10.5).isOk); // true
print(priceSchema.safeParse(-5.0).isFail); // true
```

## Cross-field rules with `.refine()`

When validation depends on multiple fields, use `.refine()` on the parent object schema.

```dart
final signUpSchema = Ack.object({
  'password': Ack.string().minLength(8),
  'confirmPassword': Ack.string().minLength(8),
}).refine(
  (data) => data['password'] == data['confirmPassword'],
  message: 'Passwords do not match',
);

final result = signUpSchema.safeParse({
  'password': 'pass1234',
  'confirmPassword': 'different',
});
print(result.isFail); // true
```

## Overriding error messages

The optional `message` parameter on `.constrain()` lets you customize the failure message.

```dart
final schema = Ack.double()
    .constrain(IsPositiveConstraint(), message: 'Price must be greater than zero.');

final result = schema.safeParse(-10.0);
if (result.isFail) {
  print(result.getError().message); // Price must be greater than zero.
}
```

## Organizing reusable constraints

Place frequently used constraints in utility files so they can be shared across schemas.

```dart
// file: validation/constraints.dart
import 'package:ack/ack.dart';

class IsPositiveConstraint extends Constraint<double> with Validator<double> {
  // ...
}

// file: schemas/user_schema.dart
import 'package:ack/ack.dart';
import '../validation/constraints.dart';

final userSchema = Ack.object({
  'age': Ack.integer(),
  'salary': Ack.double().constrain(IsPositiveConstraint()),
});
```

## When to use custom logic

- **Complex business rules:** Domain-specific checks that built-ins don’t cover.
- **Cross-field relationships:** Comparing password and confirmation fields, for example.
- **Reusable patterns:** Common rules applied across multiple schemas.
- **External service checks:** Validating against APIs or databases (beware of latency).

Chaining built-in constraints is often enough for simple cases. Use custom constraints or refinements when you need extra flexibility.

## Next steps

- [Common Recipes](./common-recipes.mdx) — custom constraints in real-world schemas
- [Flutter Form Validation](./flutter-form-validation.mdx) — surface custom messages in forms
- [Error Handling](../core-concepts/error-handling.mdx) — read `SchemaConstraintsError`
```

### Form Validation in Flutter with Ack

Source: https://docs.page/conceptadev/ack/guides/flutter-form-validation

```mdx

This guide shows how to use Ack for validating forms in Flutter applications.

## Prerequisites

- **Flutter basics**: Creating widgets, managing state, and using forms
- **Ack fundamentals**: Creating schemas and validation (see [Quickstart Tutorial](/getting-started/quickstart-tutorial))
- **Validation rules**: Built-in constraints (see [Validation Rules](/core-concepts/validation))

Install Ack in your Flutter project:

```bash
flutter pub add ack
```

## Basic form validation with `TextFormField`

Ack works with Flutter form APIs by running `safeParse` inside
`TextFormField.validator` and returning a `String?` error message
(`null` when valid).

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

class SignUpForm extends StatefulWidget {
  const SignUpForm({super.key});

  @override
  State<SignUpForm> createState() => _SignUpFormState();
}

class _SignUpFormState extends State<SignUpForm> {
  // GlobalKey to manage Form state
  final _formKey = GlobalKey<FormState>();

  // Controllers for input fields
  final _usernameController = TextEditingController();
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  // Define Ack schemas for validation rules
  // See: [Schema Types](../core-concepts/schemas.mdx), [Validation Rules](../core-concepts/validation.mdx)
  final _usernameSchema = Ack.string()
    .minLength(3)
    .maxLength(20)
    .matches(r'^[a-zA-Z0-9_]+$')
    .notEmpty();

  final _emailSchema = Ack.string()
    .email()
    .notEmpty();

  final _passwordSchema = Ack.string()
    .minLength(8)
    .matches(r'.*[A-Z].*')
    .matches(r'.*[a-z].*')
    .matches(r'.*[0-9].*')
    .notEmpty();

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey, // Associate the key with the Form
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          // Username Field
          TextFormField(
            controller: _usernameController,
            decoration: const InputDecoration(labelText: 'Username'),
            // Use the schema's validate method in the validator
            validator: (value) {
              final result = _usernameSchema.safeParse(value);
              // Return the error message if validation fails
              // See: [Error Handling](../core-concepts/error-handling.mdx)
              return result.isFail ? result.getError().toString() : null;
            },
            autovalidateMode: AutovalidateMode.onUserInteraction,
          ),
          const SizedBox(height: 16),

          // Email Field
          TextFormField(
            controller: _emailController,
            decoration: const InputDecoration(labelText: 'Email'),
            keyboardType: TextInputType.emailAddress,
            validator: (value) {
              final result = _emailSchema.safeParse(value);
              return result.isFail ? result.getError().toString() : null;
            },
            autovalidateMode: AutovalidateMode.onUserInteraction,
          ),
          const SizedBox(height: 16),

          // Password Field
          TextFormField(
            controller: _passwordController,
            decoration: const InputDecoration(labelText: 'Password'),
            obscureText: true,
            validator: (value) {
              final result = _passwordSchema.safeParse(value);
              return result.isFail ? result.getError().toString() : null;
            },
            autovalidateMode: AutovalidateMode.onUserInteraction,
          ),
          const SizedBox(height: 24),

          // Submit Button
          ElevatedButton(
            onPressed: _submitForm,
            child: const Text('Sign Up'),
          ),
        ],
      ),
    );
  }

  void _submitForm() {
    // Validate the entire form using the GlobalKey
    if (_formKey.currentState!.validate()) {
      // If the form is valid, display a Snackbar or proceed.
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Processing Data')),
      );
      print('Form is valid!');
      print('Username: ${_usernameController.text}');
      print('Email: ${_emailController.text}');
      // Usually, you would send this data to a server
    } else {
      print('Form is invalid.');
    }
  }

  @override
  void dispose() {
    // Dispose controllers when the widget is removed from the widget tree
    _usernameController.dispose();
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }
}
```

Key steps: define an `AckSchema` per field; in `TextFormField.validator`, call `schema.safeParse(value)` and return `result.getError().toString()` on failure; trigger full-form validation via `_formKey.currentState!.validate()`; set `autovalidateMode: AutovalidateMode.onUserInteraction` for real-time feedback. Pass `message:` inside the schema definition for [custom error messages](../core-concepts/error-handling.mdx#custom-error-messages).

## Real-time validation with `TextField`

Without a `Form` widget, listen to controller changes and update the error state directly.

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

class RealtimeValidationField extends StatefulWidget {
  const RealtimeValidationField({super.key});

  @override
  State<RealtimeValidationField> createState() => _RealtimeValidationFieldState();
}

class _RealtimeValidationFieldState extends State<RealtimeValidationField> {
  final _emailController = TextEditingController();
  String? _emailErrorText; // State variable to hold the error message

  // Define the schema
  final _emailSchema = Ack.string()
      .email()
      .notEmpty();

  @override
  void initState() {
    super.initState();
    // Add listener to validate on change
    _emailController.addListener(_validateEmail);
  }

  void _validateEmail() {
    final text = _emailController.text;
    // Only validate if the field is not empty (or on first interaction)
    // Adjust logic based on desired UX (e.g., validate after first blur)
    if (text.isNotEmpty) {
      final result = _emailSchema.safeParse(text);
      // Update the error state variable, triggering a rebuild
      setState(() {
        _emailErrorText = result.isFail ? result.getError().toString() : null;
      });
    } else {
      // Clear error if field becomes empty
      setState(() {
         _emailErrorText = null;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: TextField(
        controller: _emailController,
        decoration: InputDecoration(
          labelText: 'Email',
          // Display the error text from the state variable
          errorText: _emailErrorText,
        ),
        keyboardType: TextInputType.emailAddress,
      ),
    );
  }

  @override
  void dispose() {
    _emailController.removeListener(_validateEmail);
    _emailController.dispose();
    super.dispose();
  }
}
```

Key steps: hold a state variable for the error message; add a listener to `TextEditingController` in `initState`; call `schema.safeParse()` inside the listener and `setState` with the result; remove the listener in `dispose`.

## Validating entire forms on submission

Instead of validating field by field, validate the complete data structure on submission using [`Ack.object`](../core-concepts/schemas.mdx#object).

```dart
// Define a schema for the whole form data
final _formSchema = Ack.object({
  'username': _usernameSchema, // Reuse field schemas
  'email': _emailSchema,
  'password': _passwordSchema,
  'confirmPassword': Ack.string()
      .notEmpty(),
}).refine(
  (data) => data['password'] == data['confirmPassword'],
  message: 'Passwords do not match',
);

void _submitForm() {
  // Gather all form data into a map
  final formData = {
    'username': _usernameController.text,
    'email': _emailController.text,
    'password': _passwordController.text,
    'confirmPassword': _confirmPasswordController.text, // Assume this controller exists
  };

  // Validate the entire map
  final result = _formSchema.safeParse(formData);

  if (result.isOk) {
    print('Entire form data is valid!');
    // Submit formData
  } else {
    final error = result.getError();
    print('Form submission error: $error');
    // You might need to map the error path back to specific fields
    // to display errors if not using TextFormField validators.
  }
}
```

Key steps: build an [`Ack.object`](../core-concepts/schemas.mdx#object) schema that reuses your field schemas; add [`.refine()`](./custom-validation.mdx#cross-field-rules-with-refine) for cross-field rules; call `safeParse` on the full form map at submission. On failure, use [`error.path`](../core-concepts/error-handling.mdx#understanding-schemaerror) to map errors back to specific fields when not using `TextFormField`'s built-in display.
```

### JSON Schema Integration

Source: https://docs.page/conceptadev/ack/guides/json-schema-integration

```mdx

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.

## 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.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();
```
```

### Adapter Package Quickstart

Source: https://docs.page/conceptadev/ack/guides/schema-converter-quickstart

```mdx

**Use this for rapid prototyping of new schema converter packages**

This guide is a scaffold for converter authors. The generated converter examples
use `Map<String, Object?>` as a stable intermediate representation to keep this
template runnable without binding to any one target schema SDK.
If your target SDK uses native schema objects, replace each map construction with
the corresponding SDK builders.

## 1. Create Package (2 minutes)

```bash
cd packages/
mkdir ack_<target> && cd ack_<target>

# Create structure
mkdir -p lib/src test example docs
touch lib/ack_<target>.dart
touch lib/src/{converter,extension}.dart
touch test/to_<target>_schema_test.dart
touch example/basic_usage.dart
touch {README,CHANGELOG}.md
touch pubspec.yaml
touch analysis_options.yaml
touch .pubignore
```

## 2. Configure pubspec.yaml (3 minutes)

```yaml
name: ack_<target>
description: <Target> schema converter for Ack validation library
version: 1.0.0-beta.1
repository: https://github.com/btwld/ack

environment:
  sdk: '>=3.8.0 <4.0.0'
  # flutter: '>=3.16.0'  # Uncomment if needed

dependencies:
  ack: ^1.0.0
  # <target_sdk>: ^x.y.z  # Add if needed
  meta: ^1.15.0

dev_dependencies:
  test: ^1.24.0
  lints: ^5.0.0
```

## 3. Main Library File (5 minutes)

**`lib/ack_<target>.dart`**:

```dart
/// <Target> schema converter for Ack validation library.
library;

import 'package:ack/ack.dart';

export 'src/extension.dart';
```

## 4. Extension Method (3 minutes)

**`lib/src/extension.dart`**:

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

extension <Target>SchemaExtension on AckSchema {
  /// Converts this Ack schema to <Target> format.
  ///
  /// In this template, this returns a map representation so the example stays
  /// self-consistent across targets.
  Map<String, Object?> to<Target>Schema() {
    return <Target>SchemaConverter.convert(this);
  }
}
```

## 5. Converter Skeleton (10 minutes)

**`lib/src/converter.dart`**:

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

class <Target>SchemaConverter {
  const <Target>SchemaConverter._();

  /// Returns a map-based representation to avoid binding this template to one
  /// specific SDK type. Replace these map shapes with your target SDK schema types.
  static Map<String, Object?> convert(AckSchema schema) {
    return _convertSchema(schema.toSchemaModel());
  }

  static Map<String, Object?> _convertSchema(AckSchemaModel schema) {
    return switch (schema) {
      AckStringSchemaModel(allowedStringValues: final values)
          when values != null =>
        _convertEnum(schema),
      AckStringSchemaModel() => _convertString(schema),
      AckIntegerSchemaModel() => _convertInteger(schema),
      AckNumberSchemaModel() => _convertNumber(schema),
      AckBooleanSchemaModel() => _convertBoolean(schema),
      AckObjectSchemaModel() => _convertObject(schema),
      AckArraySchemaModel() => _convertArray(schema),
      AckAnyOfSchemaModel() => _convertAnyOf(schema),
      AckOneOfSchemaModel() => _convertOneOf(schema),
      AckAllOfSchemaModel() => _convertAllOf(schema),
      AckNullSchemaModel() => {'type': 'null'},
      AckRefSchemaModel(refName: final name) => {
        r'$ref': '#/definitions/$name',
      },
    };
  }

  static Map<String, Object?> _convertString(AckStringSchemaModel s) {
    return <String, Object?>{
      'type': 'string',
      if (s.description != null) 'description': s.description,
      if (s.nullable) 'nullable': true,
      if (s.minLength != null) 'minLength': s.minLength,
      if (s.maxLength != null) 'maxLength': s.maxLength,
      if (s.pattern != null && s.pattern!.isNotEmpty) 'pattern': s.pattern,
      if (s.format != null) 'format': s.format,
    };
  }

  static Map<String, Object?> _convertInteger(AckIntegerSchemaModel s) {
    return <String, Object?>{
      'type': 'integer',
      if (s.description != null) 'description': s.description,
      if (s.nullable) 'nullable': true,
      if (s.minimum != null) 'minimum': s.minimum,
      if (s.maximum != null) 'maximum': s.maximum,
      if (s.exclusiveMinimum != null) 'exclusiveMinimum': s.exclusiveMinimum,
      if (s.exclusiveMaximum != null) 'exclusiveMaximum': s.exclusiveMaximum,
    };
  }

  static Map<String, Object?> _convertNumber(AckNumberSchemaModel s) {
    return <String, Object?>{
      'type': 'number',
      if (s.description != null) 'description': s.description,
      if (s.nullable) 'nullable': true,
      if (s.minimum != null) 'minimum': s.minimum,
      if (s.maximum != null) 'maximum': s.maximum,
      if (s.exclusiveMinimum != null) 'exclusiveMinimum': s.exclusiveMinimum,
      if (s.exclusiveMaximum != null) 'exclusiveMaximum': s.exclusiveMaximum,
    };
  }

  static Map<String, Object?> _convertBoolean(AckBooleanSchemaModel s) {
    return <String, Object?>{
      'type': 'boolean',
      if (s.description != null) 'description': s.description,
      if (s.nullable) 'nullable': true,
    };
  }

  static Map<String, Object?> _convertObject(AckObjectSchemaModel s) {
    final additionalProperties = switch (s.additionalProperties) {
      AckAdditionalPropertiesAllowed() => true,
      AckAdditionalPropertiesDisallowed() => false,
      AckAdditionalPropertiesSchema(schema: final schema) =>
        _convertSchema(schema),
      null => null,
    };

    return <String, Object?>{
      'type': 'object',
      'properties': {
        for (final entry in s.properties?.entries ?? const [])
          entry.key: _convertSchema(entry.value),
      },
      if (s.required != null && s.required!.isNotEmpty) 'required': s.required,
      if (s.description != null) 'description': s.description,
      if (s.nullable) 'nullable': true,
      if (additionalProperties != null)
        'additionalProperties': additionalProperties,
    };
  }

  static Map<String, Object?> _convertArray(AckArraySchemaModel s) {
    return <String, Object?>{
      'type': 'array',
      if (s.items != null) 'items': _convertSchema(s.items!),
      if (s.description != null) 'description': s.description,
      if (s.nullable) 'nullable': true,
      if (s.minItems != null) 'minItems': s.minItems,
      if (s.maxItems != null) 'maxItems': s.maxItems,
    };
  }

  static Map<String, Object?> _convertEnum(AckStringSchemaModel s) {
    return <String, Object?>{
      'type': 'string',
      if (s.description != null) 'description': s.description,
      if (s.nullable) 'nullable': true,
      'enum': s.allowedStringValues,
    };
  }

  static Map<String, Object?> _convertAnyOf(AckAnyOfSchemaModel s) {
    return <String, Object?>{
      'type': 'anyOf',
      if (s.description != null) 'description': s.description,
      if (s.nullable) 'nullable': true,
      if (s.discriminator != null)
        'discriminator': s.discriminator!.propertyName,
      'branches': [
        for (final branch in s.schemas) _convertSchema(branch),
      ],
    };
  }

  static Map<String, Object?> _convertOneOf(AckOneOfSchemaModel s) {
    return <String, Object?>{
      'type': 'oneOf',
      if (s.description != null) 'description': s.description,
      if (s.nullable) 'nullable': true,
      'branches': [
        for (final branch in s.schemas) _convertSchema(branch),
      ],
    };
  }

  static Map<String, Object?> _convertAllOf(AckAllOfSchemaModel s) {
    return <String, Object?>{
      'type': 'allOf',
      if (s.description != null) 'description': s.description,
      if (s.nullable) 'nullable': true,
      'branches': [
        for (final branch in s.schemas) _convertSchema(branch),
      ],
    };
  }
}
```

## 6. Basic Tests (15 minutes)

**`test/to_<target>_schema_test.dart`**:

```dart
import 'package:ack/ack.dart';
import 'package:ack_<target>/ack_<target>.dart';
import 'package:test/test.dart';

void main() {
  group('to<Target>Schema()', () {
    test('converts string schema', () {
      final schema = Ack.string();
      final result = schema.to<Target>Schema();

      expect(result, isNotNull);
      // Add specific assertions
    });

    test('converts integer schema', () {
      final schema = Ack.integer();
      final result = schema.to<Target>Schema();

      expect(result, isNotNull);
    });

    test('converts object schema', () {
      final schema = Ack.object({
        'name': Ack.string(),
        'age': Ack.integer(),
      });
      final result = schema.to<Target>Schema();

      expect(result, isNotNull);
    });

    test('converts array schema', () {
      final schema = Ack.list(Ack.string());
      final result = schema.to<Target>Schema();

      expect(result, isNotNull);
    });
  });
}
```

## 7. Example Usage (5 minutes)

**`example/basic_usage.dart`**:

```dart
import 'package:ack/ack.dart';
import 'package:ack_<target>/ack_<target>.dart';

void main() {
  // Define schema
  final schema = Ack.object({
    'name': Ack.string().minLength(2),
    'email': Ack.string().email(),
    'age': Ack.integer().min(0).optional(),
  });

  // Convert
  final targetSchema = schema.to<Target>Schema();

  print('Converted: $targetSchema');
}
```

## 8. README (10 minutes)

**`README.md`**:

```markdown
# ack_<target>

<Target> schema converter for Ack.

## Installation

\`\`\`yaml
dependencies:
  ack: ^1.0.0
  ack_<target>: ^1.0.0
\`\`\`

## Usage

\`\`\`dart
import 'package:ack/ack.dart';
import 'package:ack_<target>/ack_<target>.dart';

final schema = Ack.object({
  'name': Ack.string(),
});

final targetSchema = schema.to<Target>Schema();
\`\`\`

## Limitations

- [List limitations]

## License

Part of the [Ack](https://github.com/btwld/ack) monorepo.
```

## 9. Verify Setup (2 minutes)

```bash
# Get dependencies
dart pub get

# Run tests
dart test

# Analyze
dart analyze

# Format
dart format .
```

---

## Next Steps

1. **Verify converter mappings** - Confirm required/optional fields and
   constraints map correctly for your target SDK
2. **Add comprehensive tests** - Cover all Ack schema types
3. **Document limitations** - Update README with specific constraints
4. **Add examples** - Real-world usage patterns
5. **Publish** - Once tests pass and docs are complete

## Total Time Estimate

- **Setup**: 40 minutes
- **Implementation**: 2-4 hours
- **Testing**: 2-3 hours
- **Documentation**: 1-2 hours
- **Total**: 6-10 hours for complete package

## Reference

See [Creating Schema Converter Packages](./creating-schema-converter-packages.mdx) for detailed guidance.
```
