dart_mappable
Quickstart • Documentation • Example • Package Comparison • Github
Improved json serialization and data classes with
full support for generics, inheritance, customization and more.
dart_mappable
covers all basic features (from/to json, == override, hashCode, toString(), copyWith)
while adding new or improved support for advances use-cases including generics, inheritance and
polymorphism, customization and more.
- 🎁 Everything included: Serialization, Equality, ToString, CopyWith and more.
- 🚀 Excels at complexity: It handles generics, polymorphism and multi-inheritance with ease.
- 🎛️ Highly flexible: Customize the serialization, add custom types or integrate with other packages.
- 🔥 No compromises: Its promise is that it just works, no matter what classes you throw at it.
(If you find an unsupported case, you get a cookie 🍪. And please add an issue on github.)
Quick Start
First, add dart_mappable
as a dependency, together with dart_mappable_builder
and build_runner
as a dev_dependency.
flutter pub add dart_mappable
flutter pub add build_runner --dev
flutter pub add dart_mappable_builder --dev
Next annotate your classes that you want to use with @MappableClass()
and add the
appropriate part
directive to include the generated .mapper.dart
file:
// This file is "model.dart"
import 'package:dart_mappable/dart_mappable.dart';
// Will be generated by dart_mappable
part 'model.mapper.dart';
@MappableClass()
class MyClass with MyClassMappable {
final int myValue;
MyClass(this.myValue);
}
To use a class you must:
- annotate the class with
@MappableClass()
and - apply a mixin with the name of the class plus
Mappable
.
Tip: Don't worry if the mixin don't exist at first, just run code-generation once an it will be created. The builder will also warn you if you define your class without the proper mixin.
Note: For generic classes (e.g. MyClass<T>
) make sure to also provide all type parameters
to the mixin (... with MyClassMappable<T>
).
In order to generate the serialization code, run the following command:
dart pub run build_runner build
Tip: You'll need to re-run code generation each time you are making changes to your annotated classes.
During development, you can use watch
to automatically watch your changes: dart pub run build_runner watch
.
This will generate a <filename>.mapper.dart
file for each of your files containing annotated classes.
Last step is to use the generated mappers. There are two main ways to interact with your models using this package:
- Through the generated
<ClassName>Mapper
classes, and - through the methods defined by the generated mixin.
...
void main() {
// Decode a [Map] using the [MyClassMapper] class:
var myClass = MyClassMapper.fromMap({'myValue': 123});
// Or decode directly from json:
var myClass2 = MyClassMapper.fromJson('{"myValue": 123}');
// Encode an instance of your class using the methods provided by the mixin:
var json = myClass.toJson(); // or .toMap()
// There are also implementations generated for [operator ==], [hashCode] and [toString]:
var thisIsTrue = (myClass == myClass2);
print(myClass);
// Last you can use [copyWith] to create a copy of an object:
var myClass3 = myClass.copyWith(myValue: 0);
}
Overview
To setup, annotate your model classes with @MappableClass()
and your enums with @MappableEnum()
.
Each annotation has a set of properties to configure the generated code.
@MappableClass()
class MyClass with MyClassMappable { ... }
@MappableEnum()
enum MyEnum { ... }
Tip: Check out the documentation about Models and Enums.
For deserialization, dart_mappable
will use the first available constructor of a class, but you
can use a specific constructor using the @MappableConstructor()
annotation.
@MappableClass()
class MyClass with MyClassMappable {
MyClass(); // Don't use this
@MappableConstructor()
MyClass.special(); // Use this
}
You can also annotate a single field or constructor parameter of a class using @MappableField()
to set a specific json key or add custom hooks.
@MappableClass()
class MyClass with MyClassMappable {
MyClass(this.value);
@MappableField(key: 'my_key')
String value;
}
Note: This can only be used on a field if it is directly assigned as a constructor parameter (MyClass(this.myField)
).
Setting this annotation on any other field will have no effect.
(Read Utilizing Constructors for an explanation why this is.)
Tip: Hooks are a way to customize the serialization of any field or class. Read more in the documentation about Mapping Hooks.
You can add the @MappableLib()
annotation to your library
statement to set a default configuration
for all included classes and enums, e.g. the case style for json keys.
@MappableLib(caseStyle: CaseStyle.camelCase) // will be applied to all classes
library model;
part 'model.mapper.dart';
@MappableClass() // no need to set caseStyle here
class MyClass with MyClassMappable {
...
}
Tip: Check out the documentation to see all available Configuration options.
Here are again all six annotations that you can use in your code:
@MappableClass()
can be used on a class to specify options like thecaseStyle
of the json keys, whether to ignore null values, or hooks.@MappableConstructor()
can be used on a constructor to mark this to be used for decoding. It has no properties.@MappableField()
can be used on a constructor parameter or a field to specify a json key to be used instead of the field name, or hooks.@MappableEnum()
can be used on an enum to specify themode
orcaseStyle
of the encoded enum values, or thedefaultValue
.@MappableValue()
can be used on an enum value to specify a custom encoded value to use.@MappableLib()
can be used on a library statement or import / export statement to set a default configuration for the annotated library or include / exclude classes.
Mapper Interface
dart_mappable
will generate Mapper
classes that provide these methods or properties:
<ClassName>Mapper.fromMap<T>(Map<String, dynamic> map)
will take an encoded map object and return a decoded object of typeClassName
.<ClassName>Mapper.fromJson<T>(String json)
internally usesfromMap
but works with json encodedString
s.<ClassName>Mapper.container
exposes the internalMapperContainer
for more advanced uses.
Tip: If you prefer to use MyClass.fromJson
over MyClassMapper.fromJson
, add the fromJson
and
fromMap
methods directly to your class like this:
class MyClass with MyClassMappable {
...
static final fromMap = MyClassMapper.fromMap;
static final fromJson = MyClassMapper.fromJson;
}
The generated <ClassName>Mappable
mixin will come with the following methods:
toMap()
andtoJson()
.copyWith()
to create copies of your class instance (see Copy With).- overrides for
operator ==
,hashCode
andtoString()
.
Full Documentation
See the full documentation here or jump directly to the topic you are looking for:
- Models
- Enums
- Configuration
- Copy-With
- Polymorphism
- Generics
- Mapping Hooks
- Custom Mappers
- Mapper Container
Compatibility
This package aims to be compatible with other code-generation packages. Check the examples
directory for some common use-cases.
freezed
freezed is a "code generator for unions/pattern-matching/copy"; With this package, it is easy to create union or sealed classes.
Here is a simple example taken from their documentation:
part 'myfile.freezed.dart';
@freezed
class Union with _$Union {
const factory Union(int value) = Data;
const factory Union.loading() = Loading;
const factory Union.error([String? message]) = ErrorDetails;
}
To make it compatible with dart_mappable, just add your @MappableClass
annotations to both the parent class, and all factory constructors, as if they were the child classes.
For a description of the discriminatorKey
and discriminatorValue
properties refer to the Polymorphism documentation.
You can also add the @MappableField()
annotation to any of the fields.
part 'myfile.freezed.dart';
part 'myfile.mapper.dart';
@freezed
@MappableClass(discriminatorKey: 'type')
class Union with _$Union {
@MappableClass(discriminatorValue: 'data')
const factory Union.data(@MappableField(key: 'mykey') int value) = Data;
@MappableClass(discriminatorValue: 'loading')
const factory Union.loading() = Loading;
@MappableClass(discriminatorValue: 'error')
const factory Union.error([String? message]) = ErrorDetails;
}
This will now allow you to use this and the resulting Data
, Loading
and ErrorDetails
classes as usual:
void main() {
var data = Union.data(42);
var dataJson = data.toJson();
print(dataJson); // {"mykey":42,"type":"data"}
var parsedData = UnionMapper.fromJson(dataJson);
print(parsedData); // Union.data(value: 42)
}
For the full example and generated files, check out the examples/example_freezed
directory.
json_serializable
json_serializable is a popular serialization package for simple applications. While this package was designed as a replacement / alternative for json_serializable, you may come across situations where you need to deal with classes that either use this package directly, or are designed to be compatible with it.
These classes always have the same structure:
- A factory constructor
MyClass.fromJson(Map<String, Object?> json)
to decode json to an instance of the class, and - A
myClass.toJson()
method to encode an instance to json.
To use these classes with dart_mappable
, you can use the SerializableMapper
like this:
void main() {
var myClassMapper = SerializableMapper<MyClass, Map<String, dynamic>>(
decode: MyClass.fromJson,
encode: (myClass) => myClass.toJson,
);
// This makes it accessible by all other mappers.
MapperContainer.defaults.use(myClassMapper);
}
For generic classes with one or two type parameters, use the SerializableMapper.arg1
or
SerializableMapper.arg2
constructors respectively.
fast_immutable_collections
fast_immutable_collections adds immutable
variants for the standard collections types (List
, Map
, Set
). These types are compatible with
json_serializable
, so we can use the SerializableMapper
from above as follows:
final iListMapper = SerializableMapper<IList, dynamic>.arg1(
decode: IList.fromJson,
encode: (list) => list.toJson,
type: <E>(f) => f<IList<E>>(),
);
final iMapMapper = SerializableMapper<IMap, Map<String, dynamic>.arg2(
decode: IMap.fromJson,
encode: (map) => map.toJson,
type: <Key, Val>(f) => f<IMap<Key, Val>>(),
);
For a complete working example see the fic_mappable example on Github.