feat: add flutter_codec for portable Flutter value JSON codecs - #138
Draft
leoafarias wants to merge 64 commits into
Draft
feat: add flutter_codec for portable Flutter value JSON codecs#138leoafarias wants to merge 64 commits into
leoafarias wants to merge 64 commits into
Conversation
Bulk-copy schemas/*, ack.dart, datetime_constraint.dart, discriminated_branch_utils.dart, and supporting types (common_types, context, schema_error, helpers) from claude/ack-typed-codecs-pre-rebase-backup. Delete transformed_schema.dart (backup design replaces it with WrapperSchema mixin + CodecSchema/DefaultSchema/InstanceSchema). Library down from 47 errors to 4 \xe2\x80\x94 remaining failures are in ack_schema_model_builder.dart (TransformedSchema reference, effectiveBranch missing, defaultValue removed from base). Test side still ~200 errors (API drift). Not a working checkpoint yet.
- Add copyWithInner to WrapperSchema mixin + CodecSchema/DefaultSchema - Adapt main's effectiveDiscriminatedBranch util to walk WrapperSchema instead of TransformedSchema; add discriminatorPropertyAcceptsValue and effectiveDiscriminatedObjectBranch helpers - Loosen DiscriminatedObjectSchema constructor for union-owned discriminator: branches without the discriminator are allowed (union synthesizes literal); branches with it must accept the label - Add effectiveBranch method to DiscriminatedObjectSchema - Update ack_schema_model_builder: DefaultSchema precheck for defaults, generic WrapperSchema precheck, InstanceSchema case - Drop legacy _withDefaultAndWarnings base-schema defaultValue access Library compiles cleanly (dart analyze lib: no issues found). Tests still pending.
- Pull backup's versions of overlap test files (any_of_null_and_default, core_schema, comprehensive_json_schema, discriminated_object_schema, path_preservation, schema_equality, documentation/*, integration/discriminated_child_transform) - Add backup's new test files: consolidation_test, typed_codecs_characterization_test, polish_test - Delete obsolete tests backup removed: composite_default, default_mutation, transformed_schema_default - Add JsonMap to package:ack public API exports - Fix ack_schema_model_builder_test to use AnyAckSchema instead of AckSchema<T> dart analyze passes cleanly. dart test: +838 passed / -27 failed (97% pass rate). Remaining failures are behavioral drift from PR #108's Draft-7 strictness (propertyOrdering, formatMinimum/Maximum, nested anyOf nullables); to be retargeted next.
Library changes: - ListSchema constructor rejects nullable item schemas (moved from Ack.list factory) - ack_schema_model_builder: unify DefaultSchema into the WrapperSchema path so description/nullable propagate; suppress 'x-transformed' for DefaultSchema (defaults don't transform) - ack_schema_model: AckObjectSchemaModel.toJsonSchema() omits default-bearing keys from 'required'; hoist user-facing metadata (title/description/default) to anyOf envelope top-level - ListSchema is no longer a const constructor (validation requires a body) Test alignment: - Delete strict-rejection tests for branches missing the discriminator literal (we adopted #107's union-owned discriminator) - Update 'rejects incompatible discriminator' test to expect construction-time throw - Drop _OneOfNullableStringSchema custom-schema test (toJsonSchema is now non-overridable per-schema) - Update Zod reference fixtures to omit default-bearing keys from 'required' dart analyze: clean. dart test: 862 passed / 0 failed.
- date min/max test uses local DateTime (backup's Ack.date() requires local midnight) - 'codec overrides are applied' test asserts description hoists to envelope per #108's Zod v4 convention; branches carry type info - discriminator-reject test expects construction-time throw (PR #107) 48 passed / 0 failed.
… members - Hide AnyAckSchema, Refinement, SchemaOperation from package:ack/ack.dart alongside WrapperSchema; they are internal traversal plumbing and consumers use .refine(...), safeParse, and concrete schema types instead. - Delete dead members: AckSchema.getSchemaType, AckSchemaModel.withTitle, InvalidTypeConstraint.withTypes, and the coerceJsonMap deprecated alias. - Mark CodecSchema.copyWith @internal; it duplicates copyWithRuntimeConfig and exists only to satisfy the wrapper protocol. - Add refine/constrain overrides to FluentSchema so primitive schemas preserve their concrete type through the fluent chain (matches the existing WrapperSchema behavior). Unlocks removal of two redundant `as StringSchema` casts in string_schema_extensions. - Update internal tests to consume now-hidden symbols via the src/ path.
…ugh toJsonSchema
- Rename DefaultSchema._validateDefaultWithContext to resolveDefaultWithContext
and use it from ObjectSchema and the schema model builder so the call site
expresses intent ("resolve a default") rather than the side effect of
parsing null.
- Drop the unused hasMatchingDiscriminatorLiteral helper from
discriminated_branch_utils.dart.
- Collapse toSchemaModel().toJsonSchema() to toJsonSchema() in the
ack_firebase_ai and ack_json_schema_builder adapters; update the
schema-converter guide and api-reference docs accordingly.
- Rework _dateTimeJsonFormat to read the format directly off the input
constraints, removing the dependency on AckSchemaModel construction in
datetime_schema_extensions.dart.
- Document _defaultExportContext as throwaway so the rooted error path is
intentional.
- Add focused tests: consolidation_test covers default-on-encode regressions,
polish_test distinguishes optional+codec from optional+nullable+codec,
datetime_validation_test gates date/date-time formats, and
typed_codecs_characterization_test characterizes resolveDefaultWithContext.
AnyOfSchema previously short-circuited null at the union level before trying members, so a DefaultSchema branch never got to supply its fallback. Parse now tries branches first when the input is null; the union-level null gate runs only as a fallback. Runtime validation and encode keep the original union-level gate. Also sweeps documentation for stale references caused by the typed-codec refactor: drops the removed strictParsing API, the deprecated tryParse/validate entries, and the primitive coercion language; clarifies that primitives are strict (and that integer and double do not overlap), documents Ack.number() alongside them, and updates transform examples to receive non-null runtime values.
Returns CodecSchema<String, T> wrapping EnumSchema<T> with identity decode/encode. Use when downstream code expects every value-shape to be a CodecSchema (e.g. a registry of codecs across many value shapes). The underlying EnumSchema still does the String <-> .name mapping.
Make Ack.double() and Ack.number() reject NaN and infinities by default, add fluent NumberSchema numeric constraints, and export Ack.number() as a JSON Schema number. BREAKING CHANGE: Ack.double() and Ack.number() now reject non-finite double values during parse and encode.
…raints across codecs
The .model<T>() extension on ObjectSchema duplicated the .codec<T>() API — same decode/encode signatures, with one extra behavior (omitNullOptionals=true) that silently dropped null entries from encoded JSON when the property was marked optional. That default-true null stripping violated Zod/JSON-Schema semantics: optional (key absent) and nullable (value null) are distinct states, and conflating them caused asymmetric round-trips and silent data loss. It was also redundant with ObjectSchema.encodeWithContext, which already drops null values for non-nullable properties. Removed: - ObjectSchemaModelExtension and its omitNullOptionals flag. Migrated callers: - 6 test sites switched to .codec<T>() (identical signatures).
- Shadow / BoxShadow leaf codecs (lib/src/shadows.dart) composing colorCodec, offsetCodec, and blurStyleCodec; blurRadius enforces non-negativity via Ack.number().min(0). - LinearGradient / RadialGradient / SweepGradient and a Gradient union (lib/src/gradients.dart), tagged with a 'type' discriminator via Ack.literal. colors.minItems(2); stops/focal are .nullable().optional(). transform (GradientTransform) is documented as unsupported. - Switch all flutter_codec sites from .model<T>() to .codec<T>() for uniform API; .codec already defaults output to InstanceSchema<T>. Null-optional fields now encode explicitly rather than being stripped.
The synthesized discriminator schema produced by effectiveDiscriminatedObjectBranch now carries withDefault(discriminatorValue), so a discriminated branch whose runtime encode lambda omits the discriminator key still emits it via the default. Closes the encode side of PR #107's union-owned discriminator story (parse already synthesized the literal; encode previously required the branch to emit the key). ack_schema_model_builder.\_discriminated wraps each branch's exported model with _withRequiredDiscriminator so the JSON Schema marks the discriminator as required and strips the synthetic default from the output.
- rectCodec (lib/src/primitives/rect.dart): {left, top, right, bottom} via
Rect.fromLTRB.
- imageProviderCodec (lib/src/image_providers.dart): discriminated union over
NetworkImage and AssetImage. Recursive providers, FileImage, MemoryImage, and
custom asset bundles are intentionally rejected for JSON-safety.
- networkImageCodec / assetImageCodec exported as the typed branch codecs.
- webHtmlElementStrategyCodec added to enums.dart (used by NetworkImage).
- decorationImageCodec (lib/src/decoration_image.dart): composite codec for
DecorationImage. Composes imageProviderCodec, BoxFit/ImageRepeat/FilterQuality
enums, alignmentGeometryCodec, and rectCodec. opacity is range-validated [0,1]
(stricter than Flutter, which only clamps at paint time). colorFilter and
onError are intentionally unsupported and excluded from DecorationImage's ==.
- boxDecorationCodec: 'image' field now uses decorationImageCodec.nullable()
.optional() instead of the null-only placeholder; the deferral comment is gone.
- README updated to advertise ImageProvider and the BoxDecoration.image gap is
no longer applicable.
- Doc-comment polish per Effective Dart: private helpers in font_weight,
text_decoration, locale, and text_style switched from /// to //; identifier
references throughout now use [Symbol] form; broken [_decodeLocale] reference
in locale.dart removed.
Tests: 418/418 pass (added rect_test, decoration_image_test, and image_providers
groups; updated box_decoration_test to drop 'image deferral' and add 'image
integration').
Both serialize as {tag, value} maps with a 4-character printable-ASCII
tag pattern; FontFeature.value defaults to 1, matching the constructor.
Wired into textStyleCodec so TextStyle now covers every JSON-safe
constructor field.
…ation union shapeBorderCodec discriminates the five concrete OutlinedBorder subtypes (CircleBorder, StadiumBorder, RoundedRectangleBorder, BeveledRectangleBorder, ContinuousRectangleBorder) via the 'type' key. The three rectangle-with-radius branches share a single private object schema, so the wire shape is defined exactly once. shapeDecorationCodec composes the JSON-safe ShapeDecoration constructor fields and leaves the color-XOR-gradient assert to Flutter's constructor. decorationCodec unions BoxDecoration and ShapeDecoration under the abstract Decoration type, mirroring the gradientCodec / imageProviderCodec discriminated pattern. OvalBorder (which extends CircleBorder) is documented as round-tripping to a plain CircleBorder. Also consolidates box_decoration.dart, shape_decoration.dart, and the new decoration union into a single decorations.dart, matching how gradients and image providers each live in one file with their union.
…GELOG Replaces the stale README (which incorrectly said BoxDecoration.image was deferred) with a coverage table grouped by family, a quick example, the discriminated-union summary, an intentionally-excluded list, and a short roadmap. Adds a CHANGELOG.md seeded with the 0.1.0 release scope so pub.dev renders a populated Changelog tab. No code or schema changes.
Adds roundedSuperellipseBorderCodec sharing the {side, borderRadius}
schema with the other three rectangle border codecs, and a
"roundedSuperellipse" branch on shapeBorderCodec. Bumps the Flutter
SDK floor to >=3.27.0 since RoundedSuperellipseBorder was introduced
there.
README and CHANGELOG updated; the type is no longer listed under
"intentionally excluded" or the roadmap.
…minated union branches Ack.discriminated<T> takes Map<String, AckSchema<JsonMap, T>>, and Dart's default covariance on generic parameters already accepts a CodecSchema<JsonMap, ConcreteBranchType> in place of AckSchema<JsonMap, ParentType>. The runtime encode dispatch (each branch's own validateRuntimeWithContext) checks 'value is BranchT' itself, so the .codec<Parent>(decode: (v) => v, encode: (v) => v as Concrete) wrappers were always redundant. Applies the simplification across all four union sites: - gradientCodec (3 branches) - imageProviderCodec (2 branches) - shapeBorderCodec (6 branches) - decorationCodec (2 branches) Net: ~44 lines of boilerplate removed; behavior identical. The upstream Ack.discriminatedOf<T> feature request is no longer needed.
…luded Both types keep all constructor state in library-private fields with no public getters anywhere in dart:ui or package:flutter. ColorFilter is a single class with shared runtimeType across all four constructor variants; ImageFilter is abstract with a private constructor and library-private subclasses. The only state-revealing surface is toString(), which is a debug format with no stability contract. A bidirectional codec is therefore not achievable via the public API. DecorationImage.colorFilter stays permanently excluded for the same reason. Adds a dedicated 'Opaque dart:ui state' bullet to the README's intentionally-excluded section, removes the now-obsolete Roadmap entry, and tightens decoration_image.dart's dartdoc with the concrete reason.
- textHeightBehaviorCodec covers TextHeightBehavior's three constructor fields (applyHeightToFirstAscent, applyHeightToLastDescent, leadingDistribution) with Flutter's defaults baked in. - strutStyleCodec mirrors textStyleCodec for StrutStyle, covering every JSON-safe constructor parameter. debugLabel is excluded (matches the TextStyle policy). - Extracted the package-prefix unfolding heuristic from text_style.dart into a shared lib/src/font_family_packing.dart helper, used by both textStyleCodec and strutStyleCodec. README and CHANGELOG updated. 482/482 tests pass (15 new).
…odecs
Three new codecs on lib/src/shape_borders.dart:
- starBorderCodec covers all seven StarBorder constructor parameters
(side, points, innerRadiusRatio, pointRounding, valleyRounding,
rotation in degrees, squash). StarBorder.polygon round-trips through
the regular StarBorder constructor with the computed innerRadiusRatio.
- linearBorderEdgeCodec covers LinearBorderEdge's (size, alignment),
both range-validated.
- linearBorderCodec covers LinearBorder's (side, start, end, top, bottom)
where each edge is a nullable linearBorderEdgeCodec.
shapeBorderCodec now spans eight discriminator branches (added 'star' and
'linear'). The dartdoc lists Material InputBorder subtypes as the only
remaining 'separate plan' exclusion; StarBorder and LinearBorder are
removed from the README's intentionally-excluded section.
496/496 tests pass (14 new).
- Introduced `_LazyCodec` for lazy schema resolution. - Added `boxConstraintsCodec` and `constraintsCodec` for handling Flutter's BoxConstraints. - Implemented codecs for `Matrix4`, `Container`, `Key`, and `Text` widgets. - Updated `flutter_codec.dart` to export new codecs. - Added tests for lazy codec, box constraints, matrix4, container, key, and text codecs.
Replace the local _LazyCodec shim (and its test) with Ack.lazy, now available upstream in ack. The recursion mechanic for Container.child is unchanged; only the lazy primitive moves from a private shim to the shared Ack.lazy API.
Bring branch up to current main (adds Ack.lazy and ack-core refactors). Branch contributes only the flutter_codec package; all ack core and docs are taken from main unchanged.
Required for the package to resolve under resolution: workspace.
… unencodable inputs Review-driven hardening of the flutter_codec package. - Bump Flutter floor to >=3.32.0: the package uses RoundedSuperellipseBorder and Text.semanticsIdentifier (3.32-only) and WebHtmlElementStrategy (3.29) unconditionally, so the prior >=3.27.0 would not compile on 3.27-3.31. - Fix font-family packing corruption: never infer a package when the recovered family is null (decode was folding it into the literal 'packages/<pkg>/null'). - fontWeightCodec: accept and emit integer variable-font weights [1,1000] and drop the deprecated FontWeight.index path; canonical weights still emit "wNNN". - Throw on encode for unrepresentable, equality-affecting inputs instead of dropping them silently: Gradient.transform and DecorationImage.colorFilter (and fix the false "colorFilter excluded from ==" doc). - Enforce Container / BoxDecoration / ShapeDecoration cross-field invariants with .refine so validation holds in release builds (constructor asserts are stripped). - Add readDoubleList; document the 8-bit-sRGB color loss and the StarBorder rotation/polygon lossy narrowing; sync README/CHANGELOG coverage to include the widgets/constraints/matrix4 codecs and their unions. - Add 26 regression/characterization tests (556 total). flutter analyze: clean; flutter test: 556 pass; dart format: clean.
Add a golden-fixture harness covering all 102 public codecs (56 structured
+ 46 enums). For each type it records the exact JSON the codec emits in a
reviewable per-family fixture under test/golden/fixtures/, then parses that
JSON back and asserts the round-trip:
- value equality for painting/rendering value types,
- stability (encode(parse(json)) == json) for the widget types that have
no value equality, and
- the documented narrowing for lossy types (e.g. OvalBorder -> circle).
Fixtures are kept as plain, dependency-free JSON and are byte-stable across
platforms (matrix transforms avoid trig; the only long decimal is 2*pi).
StarBorder.polygon is intentionally excluded because its encoded
innerRadiusRatio is a libm value. Regenerate with UPDATE_GOLDENS=true.
A README documents the conventions: the top-level keys are test-case
identifiers (not wire data), encode emits explicit nulls while decode treats
absent == null, unsupported fields are omitted entirely, and BoxConstraints
deliberately distinguishes an absent min bound (0) from null (infinity).
…ble state
Address confirmed codec-review findings by tightening the codec boundary
instead of silently dropping or admitting invalid runtime state:
- TextStyle.foreground/background and Text.textScaler now throw UnsupportedError
on encode rather than being silently dropped to a colorless/unscaled value.
- keyCodec matches the exact ValueKey<T> runtime type, so ValueKey subclasses
(e.g. PageStorageKey) are rejected instead of re-encoded as a plain ValueKey.
- Reject inverted BoxConstraints, negative Container padding/margin,
BoxShape.circle combined with a borderRadius, and gradient stops whose length
differs from colors.
- Constrain FontVariation.value to the [-32768, 32768) 16.16 fixed-point range.
- AlignmentDirectional center-column constants (topCenter/center/bottomCenter)
encode as {start, y} through alignmentGeometryCodec so they round-trip as
directional values.
- Tighten colorCodec rgb/rgba patterns to 0-255 so the generated JSON Schema no
longer admits out-of-range channels.
Each fix has focused regression tests. The discriminated-union ambiguity fix
lands separately via ack #115 (merged into this branch).
Adds the missing LICENSE, fixes fontFamily encoding a literal "null" string instead of JSON null, inherits the shared workspace lint config, and rounds out test coverage for gradient stop ordering and mixed geometry encode-rejection.
Disables prefer-shorthands-with-static-fields (needs Dart 3.10; the package floor is 3.8) and removes 141 explicit type arguments that the analyzer already infers, clearing the shared workspace lint config's remaining actionable findings in lib/.
Co-authored-by: Cursor <cursoragent@cursor.com>
Reconcile SDK requirements and preserve minimum Flutter CI coverage using the pinned setup action. Adapt codec validation exceptions and schema goldens to the updated ACK runtime.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
flutter_codecpackage with ACK codecs for Flutter painting, rendering, and a small widget surface (Container,Text,Key), plus goldens and workspace/CI integration.StrutStylefallback as omitted whenpackageis set, and encodingText.richreports a dedicated unsupported-span error.Test plan
cd packages/flutter_codec && dart analyze . --fatal-infoscd packages/flutter_codec && flutter test{"package": "my_pkg", "fontFamilyFallback": []}is rejected bystrutStyleCodectextWidgetCodec.safeEncode(Text.rich(...))mentions unsupported span treesMade with Cursor