codec_gen 0.9.0 copy "codec_gen: ^0.9.0" to clipboard
codec_gen: ^0.9.0 copied to clipboard

Code generator for the codec package: reads @Codable / @CodecEnum annotations and generates type-safe JSON codec fields with $.path error locations, via build_runner.

codec_gen #

pub version pub points likes license: MIT

Annotation-driven build_runner code generator for the codec runtime package.

Annotate your model with @Codable or @CodecEnum and run build_runner; codec_gen emits a _$xxxCodec static field wired to a fully type-safe Codec<T> — no hand-written encode/decode boilerplate required.

Features #

  • Annotation-driven codegen@Codable on a class generates a complete Codec<T> with both decode and encode paths.
  • Rich field control@CodecField covers renaming, decode-only aliases, typed defaults, null inclusion, custom codecs, DateTime modes, enum value mapping, and field-level schema overrides.
  • Core Dart type inferenceUri, BigInt, Duration, Set<T>, List<T>, and Map<String, T> fields are resolved automatically.
  • Generic model factories — generic @Codable classes generate codec factories such as _$pageCodec<T>(itemCodec).
  • Discriminated union codegen@CodecUnion + @CodecUnionCase generates a runtime Codec.discriminated(...) and optional oneOf schema.
  • JSON Schema generationcreateJsonSchema: true emits schema constants with $defs, generic type substitution, enum wire values, defaults, and same-name definition conflict handling.
  • Build-time validation — schema errors (unrecognised field types, missing @Codable on nested models, partial enum coverage, mismatched unknownEnumValue, invalid union cases) surface during build_runner, not at runtime.
  • Configurable exception style — set exception_style: format in build.yaml to make generated codecs throw FormatException instead of DecodeException, enabling zero-touch migration of existing error handlers.
  • Plays well with others — uses SharedPartBuilder so it coexists with json_serializable, freezed, and any other part-file generator in the same build.

Installation #

dependencies:
  codec: ^0.11.0

dev_dependencies:
  codec_gen: ^0.9.0
  build_runner: ^2.15.0

Or via the command line:

dart pub add codec
dart pub add dev:codec_gen dev:build_runner

Contents #


Annotate your model #

import 'package:codec/codec.dart';
part 'order_model.g.dart';

@Codable(includeIfNull: false)
final class OrderModel {
  final int orderId;

  @CodecField(name: 'total_amount', defaultValue: 0.0)
  final double totalAmount;

  const OrderModel({required this.orderId, required this.totalAmount});

  static final Codec<OrderModel> codec = _$orderModelCodec;
  factory OrderModel.fromJson(Object? json) =>
      codec.decode(json, typeHint: 'OrderModel');
  Object? toJson() => codec.encode(this);
}

Run the generator:

dart run build_runner build

For full annotation semantics, see the dartdoc in annotations.dart inside the codec package.


For coding agents #

When generating code in a downstream Dart / Flutter project, prefer the smallest annotation surface that preserves the project's model design:

Project need Generate with
New DTO with ordinary constructor fields @Codable()
Contract / fixture / schema export @Codable(createJsonSchema: true)
Legacy field names accepted on decode @CodecField(name: 'newName', aliases: [...])
Third-party or custom value object field @CodecField(codec: 'customCodec')
Enum represented by an int / string property @CodecField(enumValueField: 'code') or @CodecEnum(valueField: 'code')
Tagged sealed class or API event @CodecUnion(tag: ...) + @CodecUnionCase(...)

Do not hand-edit .g.dart files. Change annotations or custom codecs, then run:

dart run build_runner build
dart analyze
dart test

Generated models should expose stable entry points:

static final Codec<Model> codec = _$modelCodec;
factory Model.fromJson(Object? json) =>
    codec.decode(json, typeHint: 'Model');
Object? toJson() => codec.encode(this);

For schema-enabled models, also expose:

static const Map<String, Object?> jsonSchema = _$modelJsonSchema;

Agent-ready project guidance lives in the repository-level AGENTS.md, and the task-oriented example index lives in packages/codec_example/README.md.


Field annotation quick reference #

@Codable(includeIfNull: false)              // class-level: omit null fields in toJson by default
final class OrderModel {
  // Plain field — no annotation; uses the Dart field name as the JSON key
  final String orderId;

  // Common Dart core types are inferred automatically
  final Uri callbackUrl;
  final BigInt amountInCents;
  final Duration timeout;
  final Set<String> labels;

  // Rename + default value
  @CodecField(name: 'total_amount', defaultValue: 0.0)
  final double totalAmount;

  // Decode-only aliases for backend field migrations.
  // Decode accepts total_amount first, then amount_cents / legacy_amount;
  // encode writes only total_amount.
  @CodecField(name: 'total_amount', aliases: ['amount_cents', 'legacy_amount'])
  final BigInt totalAmountInCents;

  // Field-level override of class-level includeIfNull: keep null
  @CodecField(includeIfNull: true)
  final String? note;

  // Skip this field (two equivalent forms)
  @CodecIgnore()
  final String? _localCache;
  // or: @CodecField(ignore: true)

  // Custom codec
  @CodecField(codec: '_amountCodec')
  final Decimal price;

  // DateTime modes (no string reference needed)
  @CodecField(dateTime: DateTimeMode.utc)
  final DateTime createdAt;            // uses Codec.dateTimeUtc
  @CodecField(dateTime: DateTimeMode.seconds)
  final DateTime serverTime;           // uses Codec.dateTimeSeconds

  // Millisecond timestamp + UTC DateTime (time-zone-aware)
  @CodecField(dateTime: DateTimeMode.millisUtc)
  final DateTime txTime;               // decodes to isUtc=true; encodes back to ms integer
  @CodecField(dateTime: DateTimeMode.secondsUtc)
  final DateTime expireAt;             // same but second granularity

  // Enum mapped by a named property value (enum needs no @CodecEnum, keeping core/domain clean)
  @CodecField(enumValueField: 'code')
  final OrderState orderState;         // JSON {"orderState": 3} <-> instance whose code == 3

  // Forward-compatible unknown code: fall back to a specific enum value instead of throwing
  @CodecField(enumValueField: 'code', unknownEnumValue: StoreArea.hk)
  final StoreArea regionId;            // unrecognised / new backend code -> hk

  // const List/Map default values
  @CodecField(defaultValue: <String>[])
  final List<String> tags;
  @CodecField(defaultValue: <String, int>{})
  final Map<String, int> counters;

  // Field-level JSON Schema override for custom codec shapes.
  @CodecField(
    codec: 'paymentCodec',
    jsonSchema: {
      'oneOf': [
        {'type': 'object', 'required': ['kind', 'amount']},
        {'type': 'object', 'required': ['kind', 'reason']},
      ],
    },
  )
  final PaymentEvent payment;
}

Generated output shape #

For a non-generic class, codec_gen emits a top-level codec field and, when enabled, a schema constant:

final Codec<OrderModel> _$orderModelCodec = Codec.object<OrderModel>(...);

const Map<String, Object?> _$orderModelJsonSchema = {
  'type': 'object',
  'properties': {...},
  'required': [...],
};

Expose those generated symbols from your model class:

static final Codec<OrderModel> codec = _$orderModelCodec;
static const Map<String, Object?> jsonSchema = _$orderModelJsonSchema;

For generic classes, the generated codec is a function instead of a field. For unions, the generated base codec delegates to each branch codec and injects the discriminator during encode.


Generic models #

Generic @Codable classes generate a codec factory. Expose it from the class by passing a codec for each type parameter:

@Codable()
final class Page<T> {
  const Page({required this.items});
  final List<T> items;
  final Map<String, T?> lookup;

  static Codec<Page<T>> codec<T>(Codec<T> itemCodec) =>
      _$pageCodec<T>(itemCodec);
}

codec_gen reuses the provided type-parameter codec for T, T?, List<T>, Set<T>, and Map<String, T>.

When a concrete generic model appears inside a schema-enabled model, type parameters are substituted in the generated schema:

@Codable(createJsonSchema: true)
final class SearchResult {
  const SearchResult({required this.page});
  final Page<User> page;
}

The schema uses a concrete $defs key such as Page_User, and the items schema points at the substituted User definition instead of falling back to {}.


Discriminated unions #

Annotate the base type with @CodecUnion and each branch with @CodecUnionCase plus @Codable. Branches must be in the same library as the base union so the generator can find them deterministically:

@CodecUnion(tag: 'kind', createJsonSchema: true)
sealed class CheckoutEvent {
  const CheckoutEvent();
  static final Codec<CheckoutEvent> codec = _$checkoutEventCodec;
  static const Map<String, Object?> jsonSchema = _$checkoutEventJsonSchema;
}

@CodecUnionCase('card')
@Codable(createJsonSchema: true)
final class CardCheckout extends CheckoutEvent {
  const CardCheckout({required this.cardId, required this.amount});
  final String cardId;
  final int amount;
  static final Codec<CardCheckout> codec = _$cardCheckoutCodec;
  static const Map<String, Object?> jsonSchema = _$cardCheckoutJsonSchema;
}

@CodecUnionCase('voucher')
@Codable(createJsonSchema: true)
final class VoucherCheckout extends CheckoutEvent {
  const VoucherCheckout({required this.code, required this.labels});
  final String code;

  @CodecField(defaultValue: <String>[])
  final List<String> labels;

  static final Codec<VoucherCheckout> codec = _$voucherCheckoutCodec;
  static const Map<String, Object?> jsonSchema = _$voucherCheckoutJsonSchema;
}

The generated union codec uses Codec.discriminated and injects the tag during encode. When createJsonSchema is enabled, the generated union schema emits oneOf entries that combine the tag const with each branch schema.

Decode behavior follows the runtime Codec.discriminated contract:

  • missing or unknown discriminator values throw a path-aware DecodeException;
  • branch field failures retain the branch field path, such as $.amount;
  • encode emits the branch payload plus the configured discriminator key.

Build options (build.yaml) #

exception_style #

Controls the exception type thrown by the generated top-level codec (default: codec):

targets:
  $default:
    builders:
      codec_gen:
        options:
          exception_style: format   # throw FormatException; default codec throws DecodeException
Value Behaviour
codec (default) The generated _$xxxCodec is used directly; decode / encode throw DecodeException / EncodeException
format The generated _$xxxCodec automatically has .withFormatExceptions() appended; decode / encode throw Dart's built-in FormatException

Use exception_style: format to provide a zero-touch migration path for existing code that catches on FormatException — no changes required at call sites. When structured error handling is needed, use the default codec mode and catch on DecodeException / on EncodeException to inspect the errors, cause, and $.path location fields.

field_rename #

Sets a project-wide default field-rename strategy, applied to every @Codable class that does not specify its own fieldRename (default: none):

targets:
  $default:
    builders:
      codec_gen:
        options:
          field_rename: snake   # default for all @Codable classes

Allowed values are the FieldRename enum names: none, snake, kebab, pascal, camel, screamingSnake. An unknown value fails the build with an ArgumentError.

Precedence (highest first):

Source Wins when
@CodecField(name: 'x') a field sets an explicit JSON name
@Codable(fieldRename: X) the class explicitly sets a strategy (including FieldRename.none to opt back out of a global default)
field_rename (build.yaml) the class does not specify fieldRename
FieldRename.none nothing above applies

JSON Schema output #

Set createJsonSchema: true on a model to generate a top-level schema map next to the codec:

@Codable(createJsonSchema: true)
final class WebhookConfig {
  const WebhookConfig({required this.callbackUrl});
  final Uri callbackUrl;

  static final Codec<WebhookConfig> codec = _$webhookConfigCodec;
  static const Map<String, Object?> jsonSchema = _$webhookConfigJsonSchema;
}

The generated schema is intended for documentation, fixture checks, and contract tests. It covers object properties, required fields, nullable fields, defaults, lists, sets, maps, enums, common core types, and nested @Codable models via $defs / $ref. It does not change runtime decode or encode behavior.

Supported inference highlights:

Dart / annotation shape Schema output
String, int, double, num, bool JSON primitive schemas
Uri {'type': 'string', 'format': 'uri'}
BigInt string schema, matching decimal-string encode output
Duration integer schema, matching millisecond encode output
DateTimeMode.millisUtc / secondsUtc integer timestamp schema
List<T> / Set<T> array schema with inferred items
Map<String, T> object schema with inferred additionalProperties
nullable field type includes null, or an equivalent nullable wrapper
default value default key, preserving typed empty collections such as <String>[]
nested @Codable $ref into $defs
@CodecField(enumValueField: 'code') enum uses the encoded field values
@CodecUnion(createJsonSchema: true) oneOf with branch schemas and tag const values

Fields using @CodecField(enumValueField: 'code') emit schema enum values from that enum property instead of enum case names. For example, an int code field produces {'type': 'integer', 'enum': [1, 3]}.

Generic nested models substitute type parameters in schema output, so Page<User> can emit a $defs entry such as Page_User whose items schema references User. Definition keys use the class name when unique and add a stable suffix only when different libraries contribute models with the same class name.

For fields backed by custom codecs, discriminated unions, or other shapes that cannot be inferred from the Dart type alone, provide a complete field schema:

@CodecField(
  codec: 'paymentCodec',
  jsonSchema: {
    'oneOf': [
      {'type': 'object', 'required': ['kind', 'amount']},
      {'type': 'object', 'required': ['kind', 'reason']},
    ],
  },
)
final PaymentEvent payment;

jsonSchema is a full override for that field. It is emitted as-is and does not automatically merge nullable or default metadata; include those keys directly when the override needs them.

Definition names are stable for normal use: a unique class name is used as-is, generic instances include their type arguments, and same-name models imported from different libraries receive a deterministic suffix to avoid $defs collisions.


Codegen-time validation #

The following errors are reported during build_runner and will never be deferred to runtime:

  • @Codable not applied to a class, or the class has no unnamed constructor.
  • A field type is not recognised by codec_gen (hint: use @CodecField(codec: 'xxx') as an escape hatch).
  • A nested model type does not carry @Codable (hint: add @Codable() or supply a field-level codec).
  • A Map key type is not String.
  • Generic model fields must be expressible from generated type-parameter codecs or regular supported field types.
  • @CodecUnion not applied to a class, has no same-library @CodecUnionCase subclasses, or a case is missing @Codable.
  • @CodecUnion(createJsonSchema: true) used while a branch lacks @Codable(createJsonSchema: true).
  • Mixed String / int @CodecValue types within the same enum.
  • @CodecField(enumValueField:) applied to a non-enum field, or the enum has no such property, or the target property type is not int / String / double / num.
  • @CodecField(unknownEnumValue:) set without enumValueField, or its enum type does not match the field's enum type (the former is a misuse; the latter would generate non-compiling code).
  • @CodecEnum with only partial @CodecValue coverage: all values must be annotated or none (defaulting to .name / valueField), to prevent a runtime EncodeException on unannotated values.

Field rename rules (fieldRename) #

Set per class with @Codable(fieldRename: FieldRename.snake), or project-wide with the field_rename build option above. @Codable(fieldRename: ...) overrides the project default; omit it to inherit. All strategies use the same word-splitting rules as Lodash / inflection:

Dart field snake kebab pascal screamingSnake
userName user_name user-name UserName USER_NAME
userID user_id user-id UserId USER_ID
URLPath url_path url-path UrlPath URL_PATH
userIDValue user_id_value user-id-value UserIdValue USER_ID_VALUE
parseHTTPURLPath parse_httpurl_path parse-httpurl-path ParseHttpurlPath PARSE_HTTPURL_PATH

A purely consecutive-uppercase run with no embedded lowercase (e.g. HTTPURL) cannot be split into HTTP + URL from the string alone and is treated as a single word. If the backend uses word-separated field names, keep explicit word boundaries in the Dart field name (e.g. parseHttpUrlPath) or use @CodecField(name: '...') to specify the JSON key explicitly.


MIT © Vincen (Zhang Wenjin)

1
likes
0
points
287
downloads

Publisher

unverified uploader

Weekly Downloads

Code generator for the codec package: reads @Codable / @CodecEnum annotations and generates type-safe JSON codec fields with $.path error locations, via build_runner.

Repository (GitHub)
View/report issues

Topics

#json #codec #code-generation #build-runner #serialization

License

unknown (license)

Dependencies

analyzer, build, codec, dart_style, source_gen

More

Packages that depend on codec_gen