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 mappr
s 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 UserDto
to User
.
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.
Complex objects
Mapping works for fields with primitive types (like int
, double
, bool
, 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:
Renaming
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
or 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.
Custom mapping
And now, what if there are firstName
and 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
or 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.
Ignoring
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
or 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 favoriteColor
field.
Collections
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.
Imagine that UserDto
and User
have fields String firstName
, String? lastName
, and int age
. This is how to use it:
When mapping UserDto
→ User
with value UserDto('John', age: 42)
, the result will be User('John', 'Snow', age: 42)
because whenNull
was applied.
When mapping UserDto
→ User
with value null
, the result will be User(firstName: 'John', lastName: 'Wick', age: 55)
because whenSourceIsNull
was applied.
Generics
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 MapType
.
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 json_serializable
or equatable
, too.
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.
Final thoughts
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.