Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
60261c4
fix(ack): align wrapper and lazy nullability contracts
leoafarias Oct 26, 2026
ef3ced6
fix(ack): validate discriminated codec output branches
leoafarias Oct 26, 2026
70b4f95
fix(ack): reject lossy duration millisecond inputs
leoafarias Oct 26, 2026
5d55d34
fix(ack): preserve custom JSON Schema vocabulary semantics
leoafarias Oct 26, 2026
7dcb381
chore(release): select 2.0.0-alpha.2 candidate
leoafarias Oct 26, 2026
0b8703b
fix(generator): preserve effective schema metadata
leoafarias Oct 26, 2026
430494b
fix(generator): resolve generated value model runtime types
leoafarias Oct 26, 2026
121581d
fix(generator): validate generated names and getter references
leoafarias Oct 26, 2026
6cd0ee0
fix(generator): enforce generic schema type contracts
leoafarias Oct 26, 2026
f98a984
docs: align 2.0 integrations and converter contracts
leoafarias Oct 26, 2026
06cc637
chore(release): prepare 2.0.0-alpha.2 packages
leoafarias Oct 26, 2026
af253ab
fix(ack): discover bundled custom schema dialects
leoafarias Oct 26, 2026
a8b44f7
fix: preserve null overrides and simplify Dart ownership
leoafarias Oct 26, 2026
dd66474
fix(generator): reconcile JSON boundary and member contracts
leoafarias Oct 26, 2026
254835b
fix(generator): honor consuming null policy on boundary aliases
leoafarias Oct 26, 2026
d82f2f3
fix(generator): preserve native codec boundary inference
leoafarias Oct 26, 2026
cf2c087
fix(generator): prove preserved boundary input shapes
leoafarias Oct 26, 2026
4e1019f
chore(stack): incorporate merged release base
leoafarias Oct 26, 2026
cc80392
chore(stack): merge updated parent branch
leoafarias Oct 26, 2026
305ca19
chore(stack): merge updated parent branch
leoafarias Oct 26, 2026
9f2c9a5
chore(stack): merge updated parent branch
leoafarias Oct 26, 2026
56affdb
chore(stack): reconcile metadata with merged boundary inference
leoafarias Oct 26, 2026
3e12fbb
chore(stack): merge updated parent branch
leoafarias Oct 26, 2026
c818de2
chore(stack): merge updated parent branch
leoafarias Oct 26, 2026
0232b67
chore(stack): merge updated parent branch
leoafarias Oct 26, 2026
04dafae
chore(stack): merge updated parent branch
leoafarias Oct 26, 2026
6e26d87
chore(stack): reconcile alpha.2 notes with merged release
leoafarias Oct 26, 2026
babddea
chore(stack): merge updated parent branch
leoafarias Oct 26, 2026
43c4186
chore(stack): merge updated parent branch
leoafarias Oct 26, 2026
e5c8400
chore(stack): reconcile integration with merged release ancestry
leoafarias Oct 26, 2026
64b5e52
fix(ack): preserve strict discriminated codec outputs
leoafarias Oct 27, 2026
044b9d3
fix(generator): validate alias capture member names
leoafarias Oct 27, 2026
56a9384
fix(ack): preserve permissive intermediate codec outputs
leoafarias Oct 27, 2026
f3584be
chore(stack): carry permissive codec correction into final layer
leoafarias Oct 27, 2026
f9c16fb
fix(generator): infer native JSON union fields as JSON values
leoafarias Oct 27, 2026
d695095
test(generator): move preserved boundary fixtures to map and object r…
leoafarias Oct 27, 2026
69d3d19
fix(ack)!: narrow preserveBoundary to detached JSON-object wire views
leoafarias Oct 27, 2026
bf31163
fix(generator): infer JSON numbers in preserved wire maps as num
leoafarias Oct 27, 2026
d1f6180
fix(ack): export a non-null restriction only while null stays reachable
leoafarias Oct 27, 2026
05176c2
docs: describe wireSchema as a validating JSON wire view
leoafarias Oct 27, 2026
85fb8a5
docs: record JSON-object wire views and native union fields
leoafarias Oct 27, 2026
55e580c
fix(generator): reject decoding branches of native union fields by st…
leoafarias Oct 27, 2026
5c78d43
fix(generator): reject Ack.any and Ack.anyOf roots behind erased aliases
leoafarias Oct 27, 2026
a319d63
fix(generator): name the failing branch in native union diagnostics
leoafarias Oct 27, 2026
92a6a4d
refactor(ack): copy wire view input once
leoafarias Oct 27, 2026
6ee1702
docs(ack): note that nested wire views copy each subtree
leoafarias Oct 27, 2026
ac0bae3
test(generator): round-trip a required native union map field
leoafarias Oct 27, 2026
da7692d
fix(generator): reject native union branches with non-JSON runtime types
leoafarias Oct 27, 2026
997400c
refactor(generator): drop the static Ack.any root guard
leoafarias Oct 27, 2026
f1243b9
fix(generator): reject Ack.map roots behind erased aliases
leoafarias Oct 27, 2026
93ea3a7
docs: record the boundary wire-view layer in the workspace changelog
leoafarias Oct 27, 2026
f6bff04
chore(stack): merge updated parent branch
leoafarias Oct 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline

## 2.0.0-alpha.2

### Breaking

* Restrict `Ack.preserveBoundary` to JSON-object schemas and return detached,
unmodifiable copies of the input map. Generated preserved map fields infer
JSON numbers as `num`.

### Fixed

* Reject generator aliases whose final passthrough policy makes an inherited
Expand All @@ -30,6 +36,8 @@ See [Conventional Commits](https://conventionalcommits.org) for commit guideline
* Resolve same-build value-model and getter references, nullable nested Sets,
generated-name collisions, and application-generic nullability correctly.
* Substitute nested and recursive type bounds when resolving type-owned schemas.
* Infer native JSON `Ack.anyOf(...)` fields without a wire-view wrapper, and
validate wire views with parse semantics and no encoders.

### Documentation and release preparation

Expand Down
8 changes: 4 additions & 4 deletions docs/api-reference/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -382,10 +382,10 @@ constructors remain responsible for defensively copying direct inputs.

**Target**: Public, constructable classes

**Generates**: A public `<ClassName>Schema` facade backed by a private raw
`wireSchema` and typed codec (`_<className>Schema`), plus the `_$ClassAck` mixin
for `toJson`, `safeToJson`, `copyWith`, and deep collection-aware `==` /
`hashCode` / `toString`
**Generates**: A public `<ClassName>Schema` facade backed by a private typed
codec (`_<className>Schema`) and a `wireSchema` that validates like it but
returns the JSON map, plus the `_$ClassAck` mixin for `toJson`, `safeToJson`,
`copyWith`, and deep collection-aware `==` / `hashCode` / `toString`

Ack infers schema fields from constructor-backed fields (including Dart 3.12+
primary constructors). Required parameters, nullable types, optional parameters,
Expand Down
3 changes: 2 additions & 1 deletion docs/core-concepts/typesafe-schemas.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,8 @@ Ack generates a library-private `_profileSchema` codec variable and a public
- `ProfileSchema.toJsonSchema()` exports the boundary JSON Schema;
- `ProfileSchema.toSchemaModel()` exports Ack's canonical schema model;
- `ProfileSchema.schema` is the typed model codec (`_profileSchema`) used for composition;
- `ProfileSchema.wireSchema` is the raw structural `Map` schema.
- `ProfileSchema.wireSchema` validates like `schema` and returns the JSON map
instead of the model.

Every instantiable `@Schemable` class and implicit sealed-union branch must
be declared `final class`, initialize its fields through an unnamed generative
Expand Down
8 changes: 4 additions & 4 deletions example/lib/class_first_models.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions packages/ack/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
## 2.0.0-alpha.2

### Breaking

* Restrict `Ack.preserveBoundary` to JSON-object schemas
(`AckSchema<JsonMap, R>`), such as objects, maps, discriminated unions, and
lazy, default, or codec wrappers over them. `BoundarySchema` no longer takes
a type argument. Compose list wire views as `Ack.list(X.wireSchema)`, and use
a codec's input schema, such as `Ack.string().datetime()`, for a scalar.
* Return a detached, recursively unmodifiable copy of the input map from
`BoundarySchema` parse, runtime validation, and encode instead of the
caller's map instance.

### Fixed

* Accept union-owned discriminators in strict map-backed codec outputs while
Expand All @@ -25,6 +36,16 @@
* Discover selected custom dialects bundled beneath their own active schema
keywords, without interpreting inactive keyword payloads or annotation data
as schemas.
* Return the same JSON from `BoundarySchema` for every string-keyed map
container, including `jsonDecode` output, without running an encoder. Nested
codec values keep their wire form, one-way transforms no longer reject valid
input, and wrapped defaults are not injected.
* Validate `BoundarySchema` values with parse semantics during encode and
runtime validation, so encode no longer accepts a value that parse rejects.
* Return a schema that is already a wire view from `Ack.preserveBoundary`
unchanged instead of validating its values twice.
* Export a wrapper's non-null restriction as `allOf: [{not: {type: null}}]`
only when the narrowed schema can still accept `null`.

## 2.0.0-alpha.1

Expand Down
23 changes: 14 additions & 9 deletions packages/ack/lib/src/ack.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,16 +109,21 @@ final class Ack {
/// snapshot of the input.
static AnySchema any() => const AnySchema();

/// Validates with [schema] while returning the original boundary value.
/// Creates a wire view of a JSON object [schema]: it validates like
/// [schema] and returns the JSON map instead of the runtime value.
///
/// Nested codecs still run for validation, but their decoded runtime values
/// are discarded. This keeps wire data composable with a later typed parse.
static BoundarySchema<Boundary> preserveBoundary<
Boundary extends Object,
Runtime extends Object
>(AckSchema<Boundary, Runtime> schema) {
return BoundarySchema<Boundary>(schema);
}
/// Nested models, codecs, defaults, and refinements still run for
/// validation, but their decoded runtime values are discarded and no encoder
/// runs. Parse and encode return a detached, recursively unmodifiable copy
/// of the input map. This keeps wire data composable with a later typed
/// parse. Put `withDefault` outside the wire view; a default inside it does
/// not apply. A [schema] that is already a wire view is returned unchanged.
static BoundarySchema preserveBoundary<Runtime extends Object>(
AckSchema<JsonMap, Runtime> schema,
) => switch (schema) {
final BoundarySchema wire => wire,
_ => BoundarySchema(schema),
};

/// Creates a schema reference that is resolved lazily on first use.
///
Expand Down
19 changes: 18 additions & 1 deletion packages/ack/lib/src/schema_model/ack_schema_model_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,9 @@ final class _SchemaModelBuilder {
var layered = base
.withDescription(schema.description ?? base.description)
.withNullable(schema.isNullable);
if (schema.excludesInnerNull) layered = _excludeNull(layered);
if (schema.excludesInnerNull && _admitsNull(layered)) {
layered = _excludeNull(layered);
}
// `DefaultSchema.constraints` is a passthrough to `inner.constraints`,
// which `_build(schema.inner)` already applied. Re-running them here
// would emit duplicate warnings (e.g. datetime range under a default).
Expand Down Expand Up @@ -617,6 +619,21 @@ final class _SchemaModelBuilder {
}
}

/// Whether [model] can still accept `null` after its own nullable flag.
///
/// References, imported resources, and extension keywords are opaque here, so
/// they count as accepting `null`.
bool _admitsNull(AckSchemaModel model) {
if (model.nullable || model.extensions.isNotEmpty) return true;
return switch (model) {
AckRefSchemaModel() || AckRawSchemaModel() || AckNullSchemaModel() => true,
AckAnyOfSchemaModel(:final schemas) ||
AckOneOfSchemaModel(:final schemas) => schemas.any(_admitsNull),
AckAllOfSchemaModel(:final schemas) => schemas.every(_admitsNull),
_ => false,
};
}

AckSchemaModel _excludeNull(AckSchemaModel model) => model.withExtensions({
...model.extensions,
'allOf': [
Expand Down
93 changes: 52 additions & 41 deletions packages/ack/lib/src/schemas/boundary_schema.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
part of 'schema.dart';

/// Schema wrapper that validates with [inner] while preserving the original
/// boundary value.
/// Wire view of a JSON object schema: validates with [inner] and returns the
/// JSON instead of the runtime value.
///
/// This is useful when callers need a composable schema for validating wire
/// data without retaining any runtime values produced by nested codecs.
/// Parse, runtime validation, and encode all run the full parse of [inner]
/// (nested models, codecs, defaults, and refinements), never an encoder. Each
/// returns a detached, recursively unmodifiable copy of the validated input
/// map, so the result never depends on the input's container type and never
/// aliases the caller's map. Each wire view copies its whole subtree, so
/// compose wire views inside a plain `Ack.object(...)` rather than inside
/// another wire view.
///
/// The wrapper's own nullability decides whether `null` is accepted; `null`
/// never reaches [inner]. A default inside [inner] therefore does not apply.
/// Put `withDefault` outside the wrapper instead.
@immutable
final class BoundarySchema<Boundary extends Object>
extends AckSchema<Boundary, Boundary>
final class BoundarySchema extends AckSchema<JsonMap, JsonMap>
with
FluentSchema<Boundary, Boundary, BoundarySchema<Boundary>>,
WrapperSchema<Boundary, Boundary, BoundarySchema<Boundary>> {
FluentSchema<JsonMap, JsonMap, BoundarySchema>,
WrapperSchema<JsonMap, JsonMap, BoundarySchema> {
@override
final AckSchema<Boundary, Object> inner;
final AckSchema<JsonMap, Object> inner;

BoundarySchema(
this.inner, {
Expand All @@ -27,64 +35,67 @@ final class BoundarySchema<Boundary extends Object>
description: description ?? inner.description,
);

SchemaResult<Boundary> _validateBoundary(
SchemaResult<JsonMap> _validateBoundary(
Object? value,
SchemaContext context,
) {
final nullResult = handleNullInput(value, context);
if (nullResult != null) return nullResult;

final result = inner.parseWithContext(value, context);
// Encode leniency (for example, omitted defaults) must not let the wire
// view accept a value that its parse would reject.
final parseContext = context.operation == SchemaOperation.parse
? context
: context.createChild(
name: context.name,
schema: inner,
value: value,
pathSegment: '',
operation: SchemaOperation.parse,
);
final result = inner.parseWithContext(value, parseContext);
if (result.isFail) return SchemaResult.fail(result.getError());

final Boundary boundary;
final normalizedMap = jsonMapOrNull(value);
if (value is Boundary) {
boundary = value;
} else if (normalizedMap is Boundary) {
boundary = normalizedMap as Boundary;
} else {
final runtime = result.getOrNull()!;
final encodeContext = context.createChild(
name: context.name,
schema: inner,
value: runtime,
pathSegment: '',
operation: SchemaOperation.encode,
// Copying also canonicalizes any string-keyed map to a JsonMap.
final copy = deepUnmodifiableCopy(value);
if (copy is! JsonMap) {
return SchemaResult.fail(
_buildTypeMismatch(
expectedType: schemaType,
actualValue: value,
context: context,
),
);
final encoded = inner.encodeWithContext(runtime, encodeContext);
if (encoded.isFail) return SchemaResult.fail(encoded.getError());
boundary = encoded.getOrNull()!;
}

return applyConstraintsAndRefinements(boundary, context);
return applyConstraintsAndRefinements(copy, context);
}

@override
@protected
SchemaResult<Boundary> parseWithContext(
SchemaResult<JsonMap> parseWithContext(
Object? value,
SchemaContext context,
) => _validateBoundary(value, context);

@override
@protected
SchemaResult<Boundary> validateRuntimeWithContext(
SchemaResult<JsonMap> validateRuntimeWithContext(
Object? value,
SchemaContext context,
) => _validateBoundary(value, context);

@override
@protected
SchemaResult<Boundary> encodeWithContext(
Boundary value,
SchemaResult<JsonMap> encodeWithContext(
JsonMap value,
SchemaContext context,
) => _validateBoundary(value, context);

@override
BoundarySchema<Boundary> copyWithInner(AnyAckSchema newInner) {
return BoundarySchema<Boundary>(
newInner as AckSchema<Boundary, Object>,
BoundarySchema copyWithInner(AnyAckSchema newInner) {
return BoundarySchema(
newInner as AckSchema<JsonMap, Object>,
isNullable: isNullable,
isOptional: isOptional,
description: description,
Expand All @@ -94,14 +105,14 @@ final class BoundarySchema<Boundary extends Object>
}

@override
BoundarySchema<Boundary> copyWith({
BoundarySchema copyWith({
bool? isNullable,
bool? isOptional,
String? description,
List<Constraint<Boundary>>? constraints,
List<Refinement<Boundary>>? refinements,
List<Constraint<JsonMap>>? constraints,
List<Refinement<JsonMap>>? refinements,
}) {
return BoundarySchema<Boundary>(
return BoundarySchema(
inner,
isNullable: isNullable ?? this.isNullable,
isOptional: isOptional ?? this.isOptional,
Expand All @@ -114,7 +125,7 @@ final class BoundarySchema<Boundary extends Object>
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
if (other is! BoundarySchema<Boundary>) return false;
if (other is! BoundarySchema) return false;

return baseFieldsEqual(other) && inner == other.inner;
}
Expand Down
6 changes: 3 additions & 3 deletions packages/ack/test/json_schema_vocabulary_contract_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -260,9 +260,9 @@ void main() {
test('fluent null policies use operators from an active dialect', () {
final widened = _import({'type': 'string'}).nullable();
final narrowed = _import({}, vocabularies: []).nullable(value: false);
final wrapped = Ack.preserveBoundary(
_import({}, vocabularies: []),
).nullable(value: false);
final wrapped = _import({}, vocabularies: [])
.codec<Object>(decode: (value) => value, encode: (value) => value)
.nullable(value: false);
final defaulted = _import({
'type': ['string', 'null'],
}).withDefault('fallback');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,15 +453,22 @@ void main() {
.withDefault('ack')
.toSchemaModel();
final boundary = Ack.preserveBoundary(
Ack.string().minLength(2),
Ack.object({'name': Ack.string().minLength(2)}),
).toSchemaModel();

expect(withDefault.toJsonSchema(), {
'type': 'string',
'minLength': 2,
'default': 'ack',
});
expect(boundary.toJsonSchema(), {'type': 'string', 'minLength': 2});
expect(boundary.toJsonSchema(), {
'type': 'object',
'properties': {
'name': {'type': 'string', 'minLength': 2},
},
'required': ['name'],
'additionalProperties': false,
});
expect(withDefault.warnings, isEmpty);
expect(boundary.warnings, isEmpty);
});
Expand Down
Loading
Loading