feat: bidirectional codec support#105
Conversation
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.
|
To view this pull requests documentation preview, visit the following URL: Documentation is deployed and generated using docs.page. |
There was a problem hiding this comment.
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 propagateencode/safeEncodeacross schemas with newSchemaEncodeError. - Fold the previous transform schema into
CodecSchema(keepingTransformedSchema<I,O>as a deprecated typedef), and addAck.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. |
| @@ -1,9 +1,65 @@ | |||
| import 'dart:core'; | |||
| AckSchema unwrapDiscriminatedBranchSchema(AckSchema schema) { | ||
| var current = schema; | ||
| while (current is TransformedSchema) { | ||
| current = current.schema; | ||
| while (current is CodecSchema) { | ||
| current = current.inputSchema; | ||
| } |
| @@ -85,8 +49,61 @@ final class EnumSchema<T extends Enum> extends AckSchema<T> | |||
| ); | |||
| 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; |
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
|
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. |
Summary
Adds bidirectional codecs (
Ack.codec<I, O>(input, output, decode:, encode:)) that round-trip between a boundary typeIand a runtime typeO. Folds the previousTransformedSchemainto a unifiedCodecSchema, addsAck.instance<T>()for runtime type guards, and lifts encode support across every schema (encode/safeEncodeonAckSchema, plusSchemaEncodeError). 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