This article is part of the series Code generation in Flutter:
Automate object mapping in Dart with
auto_mappr(👈 this one)
Code generation in Flutter: generating - TBD
Code generation in Flutter: building - TBD
Think of a project that divides a project into layers that do not depend on each other. Or a project that at least tries to separate DTO objects from API calls from models used in an app. Some apps separate domain layer models into UI models, etc. In my experience, most projects meet such scenarios and, at one point or another, have to do object-to-object mapping to transfer one object to another. Some architectures, like Clean Architecture, naturally force you to such a separation. It might be tens, hundreds, or even thousands of mappings that developers have to maintain.
While object-to-object mappings are easy to write by hand, the repetition can get really boring in large numbers. And maintaining it can be hell. These objects have similar, if not exact, fields so that it can be automated well. Nonautomated mappings can get tedious to write, maintain, and, well, it’s for sure an unnecessary boilerplate. Tools that help you with that can save a lot of time!
What about existing packages?
There are several packages available for automating object-to-object mapping in Dart, and while we have tried many of them in various projects, we have found that they can be prone to bugs and lack the features we require. Despite reporting issues or contributing pull requests, we have found that these issues may go unaddressed for extended periods of time. To address these challenges, we have continued to search for better solutions that can more effectively meet our needs.
Introducing the auto_mappr package
For the reasons above and while envisioning how it would streamline our work on apps, we first tried to design a better way to do mappings. We already knew AutoMapper from C# and other similar packages, so those were a base for what we wanted. Hence, we experimented with code generation because it’s fast at runtime and makes code debugging easier for developers. We believe all packages should prioritize making their tools fast and debuggable rather than relying on hidden magical functionality.
Therefore, we decided to create the
auto_mappr package. It uses simple syntax and code generation that produces quite good results. We implemented a vast amount of features in the first version, so the novelty of the package should not limit you. So yeah, try it and let us know in GitHub issues or at our linked Discord what you think about it. You can always create an issue to request a new feature you would like or even do a PR by yourself. But do create an issue and discuss it first to ensure we are on the same page. 🙌
AutoMappr on pub.dev (NetGlade)
The initial release has many features. Let me talk a bit about them and give you a tour of how it works.
First, the package works around a mapper class; we call them
mapprs to match the package’s name. Add the
@AutoMappr annotation and a list of object-to-object mappings you want this mapper to convert, for this class. That is done by
MapType<Source, Target>(), where the source and the target are the types of objects you want to generate mapping for. In the example below, there is a mapping from
To convert one object to another, instantiate
Mappr and call the
convert method on it. Note that the
convert function has two generic parameters — source and target. If you care about strict type inference, either assign the result of converting to an explicitly typed variable or explicitly state generics. The function cannot infer the generic parameters just from the source parameter.
Mapping works for fields with primitive types (like
String, etc.) by default. But when you use complex (custom) objects, AutoMappr has to know to which target object it should map the source. That means you must also add mappings between all those nested objects you use. As before, add
MapType<Source, Target>(), where the source and the target are the types of nested objects you want to generate mapping for.
Imagine a user has a custom object with their address (its fields have only primitive types). The code would have to be changed to this:
What if there’s an
age field in
UserDto and a
myAge field in
User? Mapper cannot map fields with not matched names. To help it, you can use the
Field.from constructor to override the field mapping. The first positional argument is the target field name, and the
from argument is a source field name.
And now, what if there are
lastName fields in
UserDto, but in
User there is only the
fullName field? The mapper will not be able to generate the correct mapping. But you can create your own custom mapping. Use the
Field.custom (specialized without other arguments) constructor to customize the field mapping. The
custom argument accepts either a static function that takes a source as its parameter and outputs a target’s field type or a const value.
Sometimes you want to ignore some target field even if a source has such a field, which you do not want to map. For that case, use the
Field.ignore (specialized without other arguments) constructor to ignore the field mapping. Use the
ignore argument to specify the target field name. Note that the target field must be nullable, or the generated mapping would not be valid for apparent reasons. Let’s ignore the
All mentioned rules also apply to collections, whether a list, set, collection, or map. If you have a collection of complex objects, you have to set up an object mapping for it using
MapType, as already mentioned.
Nullability and default values
There are two possibilities for default values — the whole object or a field. When a source object is
null, to set up a default target value, use
whenSourceIsNull argument on
MapType. When a field is
null, you can set up
whenNull argument on
Field (or some of its specific constructors). The value can be either a static function without a parameter or a constant value for both cases.
User have fields
String? lastName, and
int age. This is how to use it:
User with value
UserDto('John', age: 42), the result will be
User('John', 'Snow', age: 42) because
whenNull was applied.
User with value
null, the result will be
User(firstName: 'John', lastName: 'Wick', age: 55) because
whenSourceIsNull was applied.
Models with generics are also supported. Note that for the mappr to work correctly, you must explicitly map all type arguments you’ll use. Generic arguments might be both primitive and complex objects; there should be no limitation.
Constructors and setters
By default, auto_mappr selects the constructor with the most parameters. Other not mapped fields are mapped using the target’s setters, if possible. If you want to ignore fields that you don’t want to map using setters, simply use the
ignore parameter. When you want to use a specific constructor, you can select it using the
constructor parameter on
Constructors work every combination of source and target parameters, no matter if they are positional, optional, or named. The tool will automatically assign them according to a matching name.
3rd party libraries and tools
The package can use both shared and not shared builders to generate. The default one is the shared version so that it can generate everything to the
.g.dart output. Additionally, we tested that it works with
It also works with packages that generate source or target for you, such as
drift. In that case, you have to switch to not shared builders, optionally, with also specifying dependencies on sources.
Read more about it in the chapter dedicated to customizing the build in README.
Automatic object-to-object mapping can be very useful in reducing boilerplate code, saving time, and simplifying the whole process. AutoMappr aims to help you with that. And because we use this tool in our own apps, we will continue to use it and develop it further. 🙌
Try it yourself and let us know on our Discord how you liked it or if you have ideas for improvement.