codec_gen 0.6.0
codec_gen: ^0.6.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 #
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 —
@Codableon a class generates a completeCodec<T>with bothdecodeandencodepaths. - Rich field control —
@CodecFieldcovers renaming, decode-only aliases, typed defaults, null inclusion, custom codecs,DateTimemodes, enum value mapping, and field-level schema overrides. - Core Dart type inference —
Uri,BigInt,Duration,Set<T>,List<T>, andMap<String, T>fields are resolved automatically. - Generic model factories — generic
@Codableclasses generate codec factories such as_$pageCodec<T>(itemCodec). - Discriminated union codegen —
@CodecUnion+@CodecUnionCasegenerates a runtimeCodec.discriminated(...)and optionaloneOfschema. - JSON Schema generation —
createJsonSchema: trueemits 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
@Codableon nested models, partial enum coverage, mismatchedunknownEnumValue, invalid union cases) surface duringbuild_runner, not at runtime. - Configurable exception style — set
exception_style: formatinbuild.yamlto make generated codecs throwFormatExceptioninstead ofDecodeException, enabling zero-touch migration of existing error handlers. - Plays well with others — uses
SharedPartBuilderso it coexists withjson_serializable,freezed, and any other part-file generator in the same build.
Installation #
dependencies:
codec: ^0.11.0
dev_dependencies:
codec_gen: ^0.6.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
- For coding agents
- Field annotation quick reference
- Generated output shape
- Generic models
- Discriminated unions
- Build options
- JSON Schema output
- Codegen-time validation
- Field rename rules
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:
@Codablenot 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
Mapkey type is notString. - Generic model fields must be expressible from generated type-parameter codecs or regular supported field types.
@CodecUnionnot applied to a class, has no same-library@CodecUnionCasesubclasses, or a case is missing@Codable.@CodecUnion(createJsonSchema: true)used while a branch lacks@Codable(createJsonSchema: true).- Mixed
String/int@CodecValuetypes 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 notint/String/double/num.@CodecField(unknownEnumValue:)set withoutenumValueField, or its enum type does not match the field's enum type (the former is a misuse; the latter would generate non-compiling code).@CodecEnumwith only partial@CodecValuecoverage: all values must be annotated or none (defaulting to.name/valueField), to prevent a runtimeEncodeExceptionon 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 intoHTTP+URLfrom 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)