Skip to content

feat: bidirectional codec support#105

Closed
leoafarias wants to merge 7 commits into
mainfrom
feat/codecs
Closed

feat: bidirectional codec support#105
leoafarias wants to merge 7 commits into
mainfrom
feat/codecs

Conversation

@leoafarias
Copy link
Copy Markdown
Collaborator

Summary

Adds bidirectional codecs (Ack.codec<I, O>(input, output, decode:, encode:)) that round-trip between a boundary type I and a runtime type O. Folds the previous TransformedSchema into a unified CodecSchema, adds Ack.instance<T>() for runtime type guards, and lifts encode support across every schema (encode/safeEncode on AckSchema, plus SchemaEncodeError). Built-in codecs (Ack.date(), datetime(), uri(), duration(), enumValues()) now round-trip cleanly. TransformedSchema<I,O> is preserved as a deprecated typedef for source compatibility.

Test plan

  • `dart analyze` clean
  • `dart test` from `packages/ack/`: 1069/1069 passing (covers codec parse/encode, defaults, one-way `.transform`, generic erasure, nested-codec composites, round-trips for built-in codecs, equality)

leoafarias added 4 commits May 4, 2026 15:27
Schemas are now bidirectional — a single schema definition is the source
of truth for both wire→Dart (parse) and Dart→wire (encode) conversion.

- CodecSchema<I,O> as the unified internal primitive; Ack.codec(...) for
  explicit bidirectional codecs and Ack.instance<T>() for the runtime
  side.
- encode()/safeEncode() on every AckSchema, with SchemaEncodeError for
  failures and forward-only default semantics (defaults never synthesize
  boundary data).
- .transform(fn) now produces a one-way CodecSchema (encodeFn = null);
  parse behavior unchanged. Calling encode on a transformed schema fails
  with a path-aware error pointing at Ack.codec.
- Built-in transforms (Ack.date/datetime/uri/duration) upgraded to
  proper bidirectional codecs that round-trip cleanly.
- Composite encode for Object/List/AnyOf/Discriminated/Enum with
  parse-parity error paths. EnumSchema.encode emits .name for stable
  JSON round-trips.
- TransformedSchema preserved as a @deprecated typedef alias so
  ack_firebase_ai, ack_json_schema_builder, and ack_generator stay
  source-compatible.
Pure structural simplification of the bidirectional codec code. No
behavior changes — all 1322 tests across the monorepo still pass.

- Add `SchemaEncodeError.requiredNotNull` and `.typeMismatch` factory
  constructors; replaced inline `SchemaEncodeError(...)` construction in
  the base, codec, and enum encode paths.
- Drop `DiscriminatedObjectSchema.handleNullForEncode` override — was
  a verbatim copy of the base impl.
- Extract a private `_tryEncodeBranch` helper at the top of `schema.dart`
  to share branch-trial machinery between `AnyOfSchema.encodeValue` and
  `DiscriminatedObjectSchema._encodeByBranchTrial`. The helper handles
  child-context creation, try/catch around `encodeValue`, and optional
  failure aggregation.
- Promote `CodecSchema.jsonSchemaMarker` and `CodecSchema.oneWayEncodeMessage`
  to `static const` instead of inline literals.
- Decompose `DiscriminatedObjectSchema.encodeValue` into named private
  methods (`_encodeViaDiscriminator`, `_encodeByBranchTrial`,
  `_runTypedConstraints`) — replaces an inline closure with a clearer
  `SchemaError?` sentinel.
- Replace `properties.keys.toSet()` patterns in `ObjectSchema` with
  `properties.containsKey(key)` (Dart's `Map.containsKey` is already
  O(1); drops the upfront Set allocation).
- Trim narrating comments that restated the next line of code.
- `dart format` pass (whitespace only).
The README states "encode validates runtime values and produces wire
values," but three composite encode paths quietly violated that
contract — refinements ran on encoded boundary forms, list constraints
silently skipped on type-erased lists, and discriminated Maps without a
valid discriminator could fall through to branch trial.

- ObjectSchema: object-level refinements/constraints run on the runtime
  input map before children are encoded; a `(m) => m['startsAt'] is
  DateTime` refinement now sees `DateTime`, not the encoded string.
- ListSchema: collect runtime items alongside encoded items during
  iteration so list-level constraints apply to a properly typed `List<V>`
  regardless of the runtime list's static type.
  `Ack.list(Ack.integer()).minItems(2).safeEncode(<Object?>[1])` now
  correctly fails.
- DiscriminatedObjectSchema: when the runtime value is a `Map`, missing
  or non-string discriminators fail at `#/discriminatorKey` (mirroring
  parse). Branch trial is reserved for non-Map domain objects only.
- AnyOfSchema: any-of-level refinements run on the runtime input, not
  the encoded result. Same shape of fix as ObjectSchema.
- TransformedSchema: clarify the @deprecated message and CHANGELOG to
  state that type annotations survive the typedef collapse but the old
  positional constructor and `.schema` / `.transformer` accessors do
  not.

Adds `test/schemas/codec_encode_correctness_test.dart` with 8 new red→green
pairs locking in the corrected semantics.
Apply review feedback across the codec branch:

- Rename `decodeFn`/`encodeFn` fields to `decoder`/`encoder` (nouns,
  symmetric); public `Ack.codec(decode:, encode:)` factory unchanged.
- Defaults now flow through `outputSchema.parseAndValidate` so
  constraints on the output schema apply to defaults too.
- Codec encode is now strictly typed and never falls back to parse
  coercion. Pinned by tests.
- Collapse the parse-then-encode dual-path workaround in encodeValue
  to a single side-effect-only call against the output schema.
- Add `castFail` extension on SchemaResult to replace the repeated
  `SchemaResult.fail(_.getError())` pattern across all schemas.
- Drop weak `identical()` comparison from CodecSchema ==/hashCode.
- Forward `description` through the .transform() extension.
- Misc: extract `_failDiscriminator` helper, drop dead null branch,
  privatize `_oneWayEncodeMessage`, tighten `SchemaEncodeError`
  parameter type, document `castFail` and InstanceSchema generic
  semantics, remove debug prints.

New tests cover codec defaults, runtime encode validation, generic
type checks on InstanceSchema, and composite encode round-trips.
@docs-page
Copy link
Copy Markdown

docs-page Bot commented May 5, 2026

To view this pull requests documentation preview, visit the following URL:

docs.page/btwld/ack~105

Documentation is deployed and generated using docs.page.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces bidirectional codec support to packages/ack, making schemas usable for both boundary→runtime validation (parse) and runtime→boundary serialization (encode). It unifies one-way transforms and two-way codecs under CodecSchema, adds parse-only defaults via a DefaultSchema wrapper, and removes implicit primitive coercions in favor of explicit coercion codecs.

Changes:

  • Add bidirectional codecs via Ack.codec<I, O>(...) and propagate encode/safeEncode across schemas with new SchemaEncodeError.
  • Fold the previous transform schema into CodecSchema (keeping TransformedSchema<I,O> as a deprecated typedef), and add Ack.instance<T>() for runtime type-guard schemas.
  • Remove implicit primitive coercion (use explicit Ack.intFromString(), Ack.doubleFromString(), Ack.boolFromString()), and update built-in codecs (date, datetime, uri, duration, enums) to round-trip.

Reviewed changes

Copilot reviewed 51 out of 51 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/ack/lib/src/ack.dart Adds Ack.codec, explicit coercion codecs, Ack.instance, and makes built-in adapters round-trip via codec encoders.
packages/ack/lib/src/context.dart Adds directional SchemaOperation to context for parse vs encode behavior.
packages/ack/lib/src/converters/ack_to_json_schema_model.dart Updates JSON Schema model conversion to unwrap/handle CodecSchema and DefaultSchema.
packages/ack/lib/src/constraints/validators.dart Updates documentation to reflect new validation pipeline.
packages/ack/lib/src/schemas/any_of_schema.dart Refactors union validation to separate runtime validate, boundary decodeBoundary, and encode support.
packages/ack/lib/src/schemas/any_schema.dart Aligns “any” behavior with runtime validation (no coercion) and new default wrapper approach.
packages/ack/lib/src/schemas/boolean_schema.dart Removes strict/coercing primitive parsing and aligns JSON Schema output with default wrapper approach.
packages/ack/lib/src/schemas/codec_schema.dart Introduces CodecSchema with decode/encode hooks, one-way transform behavior, and JSON Schema marker.
packages/ack/lib/src/schemas/default_schema.dart Introduces DefaultSchema wrapper for parse-side defaults (including JSON Schema default serialization).
packages/ack/lib/src/schemas/discriminated_object_schema.dart Adds encode support and runtime branch-trial logic; refactors discriminator dispatch.
packages/ack/lib/src/schemas/enum_schema.dart Splits runtime validation vs boundary decoding and adds encode boundary as .name.
packages/ack/lib/src/schemas/extensions/ack_schema_extensions.dart Updates .transform to produce a one-way CodecSchema and documents encode failure semantics.
packages/ack/lib/src/schemas/extensions/datetime_schema_extensions.dart Updates extensions to target CodecSchema rather than TransformedSchema.
packages/ack/lib/src/schemas/extensions/duration_schema_extensions.dart Updates extensions to target CodecSchema rather than TransformedSchema.
packages/ack/lib/src/schemas/extensions/object_schema_extensions.dart Removes defaultValue plumbing in favor of default wrapper.
packages/ack/lib/src/schemas/fluent_schema.dart Changes .withDefault to return a DefaultSchema wrapper.
packages/ack/lib/src/schemas/instance_schema.dart Adds InstanceSchema<T> runtime type guard schema.
packages/ack/lib/src/schemas/list_schema.dart Refactors list validation/decoding and adds encode support while enforcing non-null list items.
packages/ack/lib/src/schemas/num_schema.dart Removes implicit/strict primitive coercion support and updates JSON Schema emission accordingly.
packages/ack/lib/src/schemas/object_schema.dart Refactors object validation/decoding and adds encode support with correct path handling.
packages/ack/lib/src/schemas/schema.dart Core refactor: introduces directional operations, separates runtime validation from boundary decode/encode, adds encode APIs.
packages/ack/lib/src/schemas/schema_type.dart Removes old coercion-based SchemaType implementation (now inlined/simplified).
packages/ack/lib/src/schemas/string_schema.dart Removes strict/coercing primitive parsing support and updates JSON Schema emission.
packages/ack/lib/src/schemas/testing/testing_schemas.dart Removes legacy defaultValue wiring in test schema.
packages/ack/lib/src/schemas/transformed_schema.dart Replaces implementation with deprecated typedef alias to CodecSchema and migration guidance.
packages/ack/lib/src/utils/discriminated_branch_utils.dart Updates branch unwrapping to account for codec wrapping.
packages/ack/lib/src/validation/schema_error.dart Adds SchemaEncodeError for encode-direction failures.
packages/ack/lib/src/validation/schema_result.dart Adds castFail helper to preserve error type across generic casts.
packages/ack/CHANGELOG.md Documents new codec/encode APIs, coercion removal, default wrapper migration, and deprecations.
packages/ack/README.md Adds documentation for encode/codecs, explicit coercion, and default wrapper migration notes.
packages/ack/test/documentation/core_concepts_schemas_examples_test.dart Updates docs examples to match “no implicit coercion” semantics.
packages/ack/test/documentation/guides_custom_validation_examples_test.dart Updates docs examples for numeric type strictness changes.
packages/ack/test/documentation/overview_doc_examples_test.dart Updates docs examples to remove strict parsing references and match new behavior.
packages/ack/test/integration/discriminated_child_transform_test.dart Migrates defaults usage to .withDefault.
packages/ack/test/schemas/any_of_null_and_default_test.dart Migrates defaults usage to .withDefault.
packages/ack/test/schemas/codec_encode_correctness_test.dart Adds coverage for encode ordering/refinement semantics and discriminated dispatch strictness.
packages/ack/test/schemas/codec_schema_test.dart Adds coverage for codec parse/encode behavior, error wrapping, defaults, and built-in round-trips.
packages/ack/test/schemas/coercion_codecs_test.dart Adds tests for explicit coercion codecs and lack of implicit coercion.
packages/ack/test/schemas/composite_default_test.dart Migrates composite defaults to .withDefault and validates default cloning behavior.
packages/ack/test/schemas/composite_encode_test.dart Adds encode coverage for objects/lists/unions/discriminated/enums and nested codec behavior.
packages/ack/test/schemas/comprehensive_json_schema_test.dart Updates expectations to reflect “no implicit coercion” and explicit codecs.
packages/ack/test/schemas/core_schema_test.dart Updates core schema tests for new default wrapper and removal of strict parsing/coercion.
packages/ack/test/schemas/default_mutation_test.dart Migrates defaults to .withDefault and updates edge case semantics.
packages/ack/test/schemas/discriminated_object_schema_test.dart Migrates defaults and adds encode behavior regression coverage.
packages/ack/test/schemas/encode_schema_test.dart Adds base encode tests for primitives/null semantics/default-forward-only behavior.
packages/ack/test/schemas/instance_schema_test.dart Adds tests for Ack.instance<T>() parse/encode, refine, and nullability semantics.
packages/ack/test/schemas/json_schema_default_emission_test.dart Adds regression for JSON Schema default emission when nullable is applied after default.
packages/ack/test/schemas/path_preservation_test.dart Updates transform/default tests to use codec+default wrappers.
packages/ack/test/schemas/schema_equality_test.dart Updates equality expectations for codec/transform structure vs closure identity.
packages/ack/test/schemas/transform_flag_inheritance_test.dart Updates transform flag inheritance test names and removes debug prints.
packages/ack/test/schemas/transformed_schema_default_test.dart Migrates transformed schema default tests to .withDefault.

Comment thread packages/ack/lib/src/ack.dart Outdated
@@ -1,9 +1,65 @@
import 'dart:core';
Comment on lines 8 to 12
AckSchema unwrapDiscriminatedBranchSchema(AckSchema schema) {
var current = schema;
while (current is TransformedSchema) {
current = current.schema;
while (current is CodecSchema) {
current = current.inputSchema;
}
Comment on lines 23 to 49
@@ -85,8 +49,61 @@ final class EnumSchema<T extends Enum> extends AckSchema<T>
);
Comment on lines 90 to +100
if (!hasValue) {
// Property missing from input
if (schema.isOptional) {
// Optional field with default - pass null to trigger the child schema's
// handleNullInput, which clones and validates the default.
if (schema.defaultValue != null) {
final propertyContext = context.createChild(
name: key,
schema: schema,
value: null,
pathSegment: key,
);
final result = schema.parseAndValidate(null, propertyContext);
result.match(
onOk: (validatedValue) {
if (validatedValue != null) {
validatedMap[key] = validatedValue;
}
},
onFail: validationErrors.add,
);
}
// Optional field without default - omit from output
} else {
// Required field missing
final ce = ObjectRequiredPropertiesConstraint(
missingPropertyKey: key,
).validate(mapValue);
if (ce != null) {
validationErrors.add(
SchemaConstraintsError(
constraints: [ce],
context: context.createChild(
name: key,
schema: schema,
value: null,
pathSegment: key,
),
),
);
final result = handle(schema, null, propertyContext);
if (result.isOk) {
final validated = result.getOrNull();
if (validated != null) out[key] = validated;
} else if (schema is DefaultSchema &&
context.operation == SchemaOperation.parse) {
errors.add(result.getError());
}
continue;
leoafarias added 2 commits May 7, 2026 12:42
Resolves the 5 findings raised in the PR #105 review by tightening
discriminated-union JSON Schema emission, unwrapping wrappers consistently,
classifying enum encode errors correctly, and bringing llms.txt up to date.

- Discriminator literal-compat check applied to both toJsonSchema() and
  toJsonSchemaModel(): canonical Ack.literal(label) branches accepted,
  mismatched literals throw ArgumentError on both paths
- Shared schema wrapper-unwrap helpers (unwrapWrappers,
  providesParseDefault) replace the codec-only walker; default-wrapped
  discriminated branches now pass object-backed checks across parse,
  encode, and both JSON Schema emission paths
- EnumSchema._validateRuntime splits wrong-type from in-set checks so
  safeEncode reports SchemaEncodeError.typeMismatch instead of an
  enum constraint listing
- ObjectSchema missing-optional path short-circuits on encode and uses
  providesParseDefault for the parse-error gate so wrapper compositions
  like CodecSchema(DefaultSchema(...)) surface failed-default errors
- _serializeDefaultForJsonSchema walks wrappers, routing defaults
  through any nested codec for boundary-correct JSON Schema output
- llms.txt refreshed: Ack.codec, encode/safeEncode, SchemaEncodeError,
  Ack.instance, .transform one-way, parse-only defaults, no implicit
  coercion, TransformedSchema deprecation
@leoafarias
Copy link
Copy Markdown
Collaborator Author

Closing — approach being retired. Branch renamed to feat/legacy-codecs locally and remote feat/codecs is being deleted. Commits preserved in this closed PR's history.

@leoafarias leoafarias closed this May 11, 2026
@leoafarias leoafarias deleted the feat/codecs branch May 11, 2026 14:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants