entity_mapper 0.5.0
entity_mapper: ^0.5.0 copied to clipboard
Lightweight code generator for Clean Architecture focused projects. Automatically creates type-safe Entity ↔ Model mapping methods with dart_mappable-style patterns.
entity_mapper
Quick Start • Documentation • Example • API Docs • GitHub
Clean Architecture entity mapping made simple.
A lightweight code generator that creates type-safe Entity ↔ Model mapping methods. Perfect for Clean Architecture and Domain-Driven Design applications where your lib/domain/ layer must stay free of infrastructure dependencies.
Features #
- 🎯 Clean Architecture friendly — domain entities stay pure Dart with zero codegen dependencies; only data-layer models carry the
@MapToEntityannotation. - 🪶 Pure Dart package — no Flutter dependency. Works in Flutter apps, Dart server-side projects, and CLI tools alike.
- ⚡ Zero runtime overhead — all mapping code is generated at build time.
- 🛡️ Fully type-safe — generated code maintains complete type safety; missing or mismatched fields surface at codegen time, not runtime.
- 🔄 Primitives, nested models, and lists —
String,int,DateTime,List<T>,FooModel, andList<FooModel>are all mapped correctly. - 🌓 Nullability handled end-to-end —
String?,FooModel?, andList<FooModel>?round-trip without null-check accidents. - 🪄 Lenient required-nullable — a
requiredmodel field whose entity counterpart is missing is filled withnullwhen its type is nullable; non-nullable cases fail at codegen with a clear message. - 🧩 Just specify the entity type — one annotation, one mixin, done.
Quick Start #
Requirements: Dart SDK ≥ 3.11.0. No Flutter SDK needed (Flutter projects work too).
Add dependencies:
dependencies:
entity_mapper: ^latest
dev_dependencies:
build_runner: ^latest
Or via CLI:
dart pub add entity_mapper
dart pub add build_runner --dev
(flutter pub add works identically in Flutter projects.)
Define your pure-Dart domain entities and your data-layer models:
// lib/domain/user.dart — pure Dart, no codegen, no annotations
class User {
const User({required this.id, required this.name, required this.age});
final String id;
final String name;
final int age;
}
// lib/data/user_model.dart — annotated with @MapToEntity
import 'package:entity_mapper/entity_mapper.dart';
import '../domain/user.dart';
part 'user_model.entity_mapper.dart';
@MapToEntity(User)
class UserModel with UserEntityMappable {
const UserModel({required this.id, required this.name, required this.age});
final String id;
final String name;
final int age;
}
Generate the mapping code:
dart run build_runner build
Use the generated API:
const user = User(id: 'u1', name: 'Alice', age: 30);
// Entity → Model
final model = UserEntityMapper.toModel(user);
// Model → Entity (via mixin)
final back = model.toEntity();
// Or via the static API
final back2 = UserEntityMapper.toEntity(model);
Overview #
The annotation #
@MapToEntity(EntityType) tells the generator which domain entity this model maps to. Apply it to your data-layer model and mix in <Model>EntityMappable:
@MapToEntity(User)
class UserModel with UserEntityMappable {
// ...
}
The mixin name is derived by stripping the Model suffix from your class name and adding EntityMappable. UserModel → UserEntityMappable. AddressModel → AddressEntityMappable.
Generated API #
For every annotated XModel targeting entity X, the generator produces:
Static mapper class — XEntityMapper:
| Member | What it does |
|---|---|
XEntityMapper.toModel(X entity) |
Convert an entity instance to its model. |
XEntityMapper.toEntity(XModel model) |
Convert a model instance to its entity. |
XEntityMapper.ensureInitialized() |
Returns a singleton accessor. Useful when wiring DI or asserting the mapper is loaded. |
Mixin — XEntityMappable:
| Member | What it does |
|---|---|
model.toEntity() |
Instance-side convenience for XEntityMapper.toEntity(model). |
Field mapping rules #
The generator handles every common field shape automatically. Below is the full table of what it emits.
| Field shape on the model | What the generator emits (entity → model direction) |
|---|---|
Primitive (String, int, bool, DateTime, etc.) |
entity.field |
XModel (non-list nested model) |
XEntityMapper.toModel(entity.field) |
XModel? |
entity.field == null ? null : XEntityMapper.toModel(entity.field!) |
List<XModel> |
entity.field.map(XEntityMapper.toModel).toList() |
List<XModel>? |
entity.field?.map(XEntityMapper.toModel).toList() |
List<String> (primitive list) |
entity.field (passthrough, no mapping) |
The reverse direction (toEntity) emits the mirror of each rule.
Naming convention #
The generator detects "this is a nested model field" by checking that the field's type name ends with Model. So EngineModel triggers nested mapping; Engine does not. Stick to the <EntityName>Model convention and everything works automatically.
Advanced usage #
Non-list nested model #
@MapToEntity(Car)
class CarModel with CarEntityMappable {
const CarModel({required this.brand, required this.engine});
final String brand;
final EngineModel engine; // generator emits EngineEntityMapper.toModel/toEntity
}
@MapToEntity(Engine)
class EngineModel with EngineEntityMappable {
const EngineModel({required this.type, required this.horsepower});
final String type;
final int horsepower;
}
List of nested models #
@MapToEntity(User)
class UserModel with UserEntityMappable {
const UserModel({required this.id, required this.addresses});
final String id;
final List<AddressModel> addresses;
}
Nullability — every variant supported #
@MapToEntity(Profile)
class ProfileModel with ProfileEntityMappable {
const ProfileModel({
this.id,
this.address,
this.tags,
});
final String? id; // null preserved both ways
final AddressModel? address; // null-or-nested, generator emits a null guard
final List<AddressModel>? tags; // null-or-list, generator emits `?.map(...)`
}
Required-nullable field with no entity counterpart #
If your model declares required parameters and some of them have no field on the entity, the generator behaves as follows:
- Nullable field type → the generator inserts
nullso therequiredkeyword is satisfied. - Non-nullable field type → codegen fails with a clear
InvalidGenerationSourceErrornaming the offending field — fix at the source rather than letting broken code ship.
// Entity has only id + name.
class PartialUser {
const PartialUser({required this.id, required this.name});
final String id;
final String name;
}
// Model declares an extra required-nullable field.
@MapToEntity(PartialUser)
class PartialUserExtraModel with PartialUserExtraEntityMappable {
const PartialUserExtraModel({
required this.id,
required this.name,
required this.extra, // generator passes `null` here
});
final String id;
final String name;
final String? extra;
}
Strictness on the other direction #
If the entity has a required field with no counterpart on the model, codegen fails immediately. Models must be capable of capturing all of an entity's required state.
Use with Clean Architecture #
Recommended directory layout:
lib/
├── domain/
│ └── entities/ # Pure Dart. No annotations. No codegen.
│ └── user.dart
├── data/
│ ├── models/ # Annotated with @MapToEntity.
│ │ └── user_model.dart
│ └── repositories/
│ └── user_repository_impl.dart
└── ...
Why this works well:
lib/domain/has zero dependency onentity_mapper. The domain layer can be unit-tested with no codegen step and reused in pure-Dart contexts.lib/data/knows about both the entity (imports fromdomain/) and the generated mapper. Repository implementations callUserEntityMapper.toModel(...)/model.toEntity()at the layer boundary.- Adding
dart_mappableto the same model class is supported —UserModelcan simultaneously mix inUserEntityMappable(for the entity bridge) andUserModelMappable(forfromJson/toJson/copyWith).
API Reference #
Full API documentation is available at pub.dev documentation.
@MapToEntity(Type entityType) #
| Parameter | Type | Description |
|---|---|---|
entityType |
Type |
The entity class this model maps to. Must be a class, not a typedef. |
@MapToEntity(User)
class UserModel with UserEntityMappable {
// ...
}
Generated members #
See the Generated API table above.
Examples #
The example/main.dart file is a single runnable program that demonstrates every supported scenario — primitives, non-list nested models, list-of-nested, nullable variants, required-nullable with missing entity counterpart, and the mixin-vs-static API. Run it from the package root:
dart run build_runner build
dart run example/main.dart
Compatibility #
| Tool | Minimum | Tested up to |
|---|---|---|
| Dart SDK | 3.11.0 | 3.11.x |
analyzer |
10.0.0 | 13.0.0 |
build |
3.0.0 | 4.x |
source_gen |
3.1.0 | 4.x |
Older analyzer versions are unsupported — entity_mapper 0.5.0 uses the canonical (unsuffixed) Element API.
Contributing #
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
To run the test suite (44 tests covering every supported scenario):
dart run build_runner build
dart test
License #
This project is licensed under the terms of the MIT License.
Support & Contact #
For issues, feature requests, or questions, please open a GitHub Issue.
Changelog #
See CHANGELOG.md for release notes and version history.
Keywords #
#clean-architecture #entity-mapping #code-generation #domain-driven-design #ddd #build-runner #source-gen #dart