Skip to content

[Feature request] Support Dart extension types in code generation #348

@definev

Description

@definev

Hi, first of all, thank you for dart_mappable — it has become one of the most ergonomic codegen libraries in the Dart ecosystem.

I would like to request support for Dart extension types.

Extension types are now a first-class language feature in Dart and are intended to provide a zero-cost, compile-time abstraction over an underlying representation type. They are especially relevant for value objects, domain-specific wrappers, and JS interop style APIs. ([dart.dev][1])

At the moment, dart_mappable already supports many advanced language and modeling scenarios, but I could not find obvious support or an existing dedicated issue for extension types in the current public issue list and package docs/changelog snippets I checked. ([GitHub][2])

Motivation

Extension types are a very natural fit for serialization/mapping because they are often used to model:

  • strongly typed IDs
  • validated scalar wrappers
  • domain-specific units
  • lightweight wrappers around primitives or collection-like types
  • transport-safe representations without heap-allocation overhead

Today, users likely need to manually bridge these types using hooks/converters or fall back to wrapper classes. Native support would make these models feel much more seamless.

Example use cases

1. Strongly typed identifiers

extension type UserId(String value) {
  @override
  String toString() => value;
}

@MappableClass()
class User with UserMappable {
  final UserId id;
  final String name;

  const User({
    required this.id,
    required this.name,
  });
}

Expected behavior:

  • encode UserId as its representation type (String)
  • decode String back into UserId

Desired JSON:

{
  "id": "user_123",
  "name": "Duong"
}

2. Domain value objects over primitives

extension type EmailAddress(String value) {
  bool get isValid => value.contains('@');
}

This is a common pattern for domain modeling. It would be ideal if dart_mappable treated this similarly to a scalar value object.

3. Numeric/unit wrappers

extension type Meters(double value) {
  double toKm() => value / 1000;
}

Useful for API models where transport uses a primitive but the domain wants stronger semantics.

4. Transparent wrappers that implement the representation type

extension type UserId(String value) implements String {}

For transparent extension types, it may be possible to treat them even closer to the representation type during mapper resolution.

5. Nested use inside collections / generics

@MappableClass()
class Team with TeamMappable {
  final List<UserId> memberIds;
  const Team(this.memberIds);
}

Support here would be especially valuable, because this is where manual conversion becomes repetitive.


Possible behavior model

A reasonable first version could treat extension types as scalar-like wrappers over their representation type.

For an extension type like:

extension type UserId(String value) {}

the generated mapper could conceptually behave like:

  • encode: UserId -> String
  • decode: String -> UserId

That would align with how extension types are intended to act as a static abstraction over an existing representation type. ([dart.dev][1])

Potential implementation directions

Option A — Treat extension types as mappable value wrappers automatically

The builder detects extension type X(T representation) and generates a mapper equivalent to a custom scalar mapper.

Conceptually:

class UserIdMapper extends SimpleMapper1<UserId, String> {
  const UserIdMapper();

  @override
  UserId decode(String value) => UserId(value);

  @override
  String encode(UserId self) => self.value;
}

Pros:

  • most ergonomic for users
  • works naturally in fields, lists, maps, nested generics
  • matches the common “strong typedef/value object” use case

Cons:

  • builder must introspect extension type declarations correctly
  • access to the representation field/member may need care depending on syntax and visibility rules

Option B — Add an annotation specifically for extension types

Something like:

@MappableExtensionType()
extension type UserId(String value) {}

Pros:

  • explicit opt-in
  • easier migration story
  • less risk of surprising behavior

Cons:

  • slightly more API surface
  • less “it just works”

Option C — Support via generated mapper hook only

Allow or document a standard way to annotate/register an extension type so dart_mappable generates only the bridging mapper and then reuses it transitively in classes.

Pros:

  • narrower scope
  • easier initial rollout

Cons:

  • weaker DX than first-class support
  • users still need extra boilerplate for a very common pattern

Suggested decoding/encoding rules

A possible set of rules:

  1. If a field type is an extension type over T, encode it exactly as T.

  2. Decode from T into the extension type constructor.

  3. Collections and generics should recurse normally:

    • List<UserId>List<String>
    • Map<String, UserId>Map<String, String>
  4. Nullable variants should work naturally:

    • UserId?String?
  5. Errors should be surfaced using the same decode error model as other scalar conversions.


Edge cases worth considering

1. Multiple constructors / custom factories

Some extension types may define extra constructors or factories. It may be best to use the primary representation constructor by default.

2. Private/internal representation details

If codegen cannot safely access the representation member, support may need to be limited to public/simple forms first.

3. Non-trivial extension types

Some extension types are not just “value wrappers” and expose a constrained or altered interface. In those cases, mapping by raw representation may still be correct, but the rules should be explicit.

4. Transparent vs non-transparent extension types

Since extension types can either implement the representation type or remain distinct from it, mapper resolution should probably be based on the declared extension type itself, not only assignability. ([dart.dev][1])

5. JS interop-oriented extension types

Some extension types are intended for dart:js_interop and may not be meaningful for JSON serialization. It may be worth documenting that only representation-backed, serializable extension types are supported. Dart’s own docs highlight JS interop as a major use case for extension types. ([dart.dev][3])


Why this would be valuable

dart_mappable already positions itself as supporting advanced use cases like generics, inheritance, and richer data-class workflows. Extension type support feels like a natural next step for modern Dart domain modeling. ([Dart packages][4])

This would let users model APIs more safely without sacrificing performance or falling back to wrapper classes just for serialization.


Minimal desired outcome

Even a limited first version would already be very useful if it supported:

  • extension types over primitive representation types
  • automatic encode/decode by representation type
  • usage inside annotated classes and collections
  • nullable support

For example:

extension type UserId(String value) {}

@MappableClass()
class User with UserMappable {
  final UserId id;
  const User(this.id);
}

with generated mapping equivalent to:

UserMapper.fromMap({'id': 'abc'}) == User(UserId('abc'));
User(UserId('abc')).toMap() == {'id': 'abc'};

Thanks for considering this feature.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions