/dart-json-mapper

Serialize / Deserialize Dart Objects to / from JSON

Primary LanguageDartBSD 3-Clause "New" or "Revised" LicenseBSD-3-Clause

Build Status pub package Build Status

This package allows programmers to annotate Dart classes in order to Serialize / Deserialize them to / from JSON.

Why?

  • Compatible with all Dart platforms, including Flutter and Web platforms
  • No need to extend your classes from any mixins/base/abstract classes to keep code leaner
  • Clean and simple setup, transparent and straight-forward usage with no heavy maintenance
  • Feature parity with highly popular Java Jackson, and only 4 annotations to remember, to cover all possible use cases.
  • No extra boilerplate, 100% generated code, which you'll never see.
  • Complementary adapters full control over the process when you strive for maximum flexibility.
  • NO dependency on dart:mirrors, one of the reasons is described here.
  • Because Serialization/Deserialization is NOT a responsibility of your Model classes.

Dart classes reflection mechanism is based on reflectable library. This means "extended types information" is auto-generated out of existing Dart program guided by the annotated classes only, as the result types information is accessible at runtime, at a reduced cost.

Typical flutter.dev project integration sample can be found here

Basic setup

Please add the following dependencies to your pubspec.yaml:

dependencies:
  dart_json_mapper:
dev_dependencies:
  build_runner:

Say, you have a dart program main.dart having some classes intended to be traveling to JSON and back.

  • First thing you should do is to put @jsonSerializable annotation on each of those classes
  • Next step is to auto generate main.reflectable.dart file. And afterwards import that file into main.dart

lib/main.dart

import 'package:dart_json_mapper/dart_json_mapper.dart' show JsonMapper, jsonSerializable, JsonProperty;

import 'main.reflectable.dart' show initializeReflectable;

@jsonSerializable // This annotation let instances of MyData travel to/from JSON
class MyData {
  int a = 123;

  @JsonProperty(ignore: true)
  bool b;

  @JsonProperty(name: 'd')
  String c;

  MyData(this.a, this.b, this.c);
}

main() {
  initializeReflectable();
  
  print(JsonMapper.serialize(MyData(456, true, "yes")));
}

output:

{ 
  "a": 456,
  "d": "yes"
}

Go ahead and create a build.yaml file in your project root directory. Then add the following content:

targets:
  $default:
    builders:
      reflectable:
        generate_for:
          - lib/main.dart

Now run the code generation step with the root of your package as the current directory:

> pub run build_runner build

You'll need to re-run code generation each time you are making changes to lib/main.dart So for development time, use watch like this

> pub run build_runner watch

Each time you modify your project code, all *.reflectable.dart files will be updated as well.

  • Next step is to add "*.reflectable.dart" to your .gitignore
  • And this is it, you are all set and ready to go. Happy coding!

Format DateTime / num types

In order to format DateTime or num instance as a JSON string, it is possible to provide intl based formatting patterns.

DateTime

@JsonProperty(converterParams: {'format': 'MM-dd-yyyy H:m:s'})
DateTime lastPromotionDate = DateTime(2008, 05, 13, 22, 33, 44);

@JsonProperty(converterParams: {'format': 'MM/dd/yyyy'})
DateTime hireDate = DateTime(2003, 02, 28);

output:

{
"lastPromotionDate": "05-13-2008 22:33:44",
"hireDate": "02/28/2003"
}

num

@JsonProperty(converterParams: {'format': '##.##'})
num salary = 1200000.246;

output:

{
"salary": "1200000.25"
}

As well, it is possible to utilize converterParams map to provide custom parameters to your custom converters.

Get or Set fields

When relying on Dart getters / setters, no need to annotate them. But when you have custom getter / setter methods, you should provide annotations for them.

@jsonSerializable
class AllPrivateFields {
  String _name;
  String _lastName;

  set name(dynamic value) {
    _name = value;
  }

  String get name => _name;

  @JsonProperty(name: 'lastName')
  void setLastName(dynamic value) {
    _lastName = value;
  }

  @JsonProperty(name: 'lastName')
  String getLastName() => _lastName;
}

// given
final json = '''{"name":"Bob","lastName":"Marley"}''';

// when
final instance = JsonMapper.deserialize<AllPrivateFields>(json);

// then
expect(instance.name, 'Bob');
expect(instance.getLastName(), 'Marley');

// when
final targetJson = JsonMapper.serialize(instance, SerializationOptions(indent: ''));

// then
expect(targetJson, json);

Example with immutable class

enum Color { Red, Blue, Green, Brown, Yellow, Black, White }

@jsonSerializable
class Car {
    @JsonProperty(name: 'modelName')
    String model;
    
    @JsonProperty(enumValues: Color.values)
    Color color;
    
    @JsonProperty(ignore: true)
    Car replacement;
    
    Car(this.model, this.color);
}

@jsonSerializable
class Immutable {
    final int id;
    final String name;
    final Car car;
    
    const Immutable(this.id, this.name, this.car);
}

print(
  JsonMapper.serialize(
    Immutable(1, 'Bob', Car('Audi', Color.Green))
  )
);

output:

{
 "id": 1,
 "name": "Bob",
 "car": {
  "modelName": "Audi",
  "color": "Color.Green"
 }
}

Constructor parameters

Sometimes you don't really care or don't want to store some json property as a dedicated class field, but instead, you would like to use it's value in constructor to calculate other class properties. This way you don't have a convenience to annotate a class field, but you could utilize constructor parameter for that.

With the input JSON like this:

{"LogistikTeileInOrdnung":"true"}

You could potentially have a class like this:

@jsonSerializable
class BusinessObject {
  final bool logisticsChecked;
  final bool logisticsOK;

  BusinessObject()
      : logisticsChecked = false,
        logisticsOK = true;

  @jsonConstructor
  BusinessObject.fromJson(
      @JsonProperty(name: 'LogistikTeileInOrdnung') String processed)
      : logisticsChecked = processed != null && processed != 'null',
        logisticsOK = processed == 'true';
}

Unmapped properties

If you are looking for an alternative to Java Jackson @JsonAnySetter / @JsonAnyGetter It is possible to configure the same scenario as follows:

@jsonSerializable
class UnmappedProperties {
  String name;

  Map<String, dynamic> _extraPropsMap = {};

  @jsonProperty
  void unmappedSet(String name, dynamic value) {
    _extraPropsMap[name] = value;
  }

  @jsonProperty
  Map<String, dynamic> unmappedGet() {
    return _extraPropsMap;
  }
}

// given
final json = '''{"name":"Bob","extra1":1,"extra2":"xxx"}''';

// when
final instance = JsonMapper.deserialize<UnmappedProperties>(json);

// then
expect(instance.name, 'Bob');
expect(instance._extraPropsMap['name'], null);
expect(instance._extraPropsMap['extra1'], 1);
expect(instance._extraPropsMap['extra2'], 'xxx');

Iterable types

Since Dart language has no possibility to create typed iterables dynamically, it's a bit of a challenge to create exact typed lists/sets/etc via reflection approach. Those types has to be declared explicitly.

For example List() will produce List<dynamic> type which can't be directly set to the concrete target field List<Car> for instance. So obvious workaround will be to cast List<dynamic> => List<Car>, which can be performed as List<dynamic>().cast<Car>().

In order to do so, we'll use Value Decorator Function inspired by Decorator pattern.

final String json = '[{"modelName": "Audi", "color": "Color.Green"}]';
JsonMapper().useAdapter(JsonMapperAdapter(
  valueDecorators: {
    typeOf<List<Car>>(): (value) => value.cast<Car>(),
    typeOf<Set<Car>>(): (value) => value.cast<Car>()
  })
);

final myCarsList = JsonMapper.deserialize<List<Car>>(json);
final myCarsSet = JsonMapper.deserialize<Set<Car>>(json);

Basic iterable based generics using Dart built-in types like List<num>, List<String>, List<bool>, List<DateTime>, Set<num>, Set<String>, Set<bool>, Set<DateTime>, etc. supported out of the box.

For custom iterable types like List<Car> / Set<Car> you have to provide value decorator function as showed in a code snippet above before using deserialization. This function will have explicit cast to concrete iterable type.

OR an easy case

When you are able to pre-initialize your Iterables with an empty instance, like on example below, you don't need to mess around with value decorators.

@jsonSerializable
class Item {}

@jsonSerializable
class IterablesContainer {
  List<Item> list = [];
  Set<Item> set = {};
}

// given
final json = '''{"list":[{}, {}],"set":[{}, {}]}''';

// when
final target = JsonMapper.deserialize<IterablesContainer>(json);

// then
expect(target.list, TypeMatcher<List<Item>>());
expect(target.list.first, TypeMatcher<Item>());
expect(target.list.length, 2);

expect(target.set, TypeMatcher<Set<Item>>());
expect(target.set.first, TypeMatcher<Item>());
expect(target.set.length, 2);

List of Lists of Lists ...

Using value decorators, it's possible to configure nested lists of virtually any depth.

@jsonSerializable
class Item {}

@jsonSerializable
@Json(valueDecorators: ListOfLists.valueDecorators)
class ListOfLists {
  static Map<Type, ValueDecoratorFunction> valueDecorators() =>
      {
        typeOf<List<List<Item>>>(): (value) => value.cast<List<Item>>(),
        typeOf<List<Item>>(): (value) => value.cast<Item>()
      };
  
  List<List<Item>> lists;
}

// given
final json = '''{
 "lists": [
   [{}, {}],
   [{}, {}, {}]
 ]
}''';

// when
final target = JsonMapper.deserialize<ListOfLists>(json);

// then
expect(target.lists.length, 2);
expect(target.lists.first.length, 2);
expect(target.lists.last.length, 3);
expect(target.lists.first.first, TypeMatcher<Item>());
expect(target.lists.last.first, TypeMatcher<Item>());

Enum types

Enum construction in Dart has a specific meaning, and has to be treated accordingly.

Generally, we always have to bear in mind following cases around Enums:

  • Your own Enums declared as part of your program code, thus they can be annotated.
  • Enums from third party packages, they can not be annotated.

So whenever possible, you should annotate your Enum declarations as follows

@jsonSerializable
@Json(enumValues: Color.values)
enum Color { Red, Blue, Green, Brown, Yellow, Black, White }

And annotate class fields referencing Enums as follows

@JsonProperty(enumValues: Color.values)
Color color;

@JsonProperty(enumValues: Color.values)
List<Color> colors;

@JsonProperty(enumValues: Color.values)
Set<Color> colorsSet;

@JsonProperty(enumValues: Color.values)
Map<Color, int> colorPriorities = <Color, int>{};

Each enum based class field has to be annotated as showed in a snippet above. Enum.values refers to a list of all possible enum values, it's a handy built in capability of all enum based types. Without providing all values it's not possible to traverse it's values properly.

Inherited classes derived from abstract / base class

Please use complementary @Json(typeNameProperty: 'typeName') annotation for subclasses derived from abstract or base class. This way dart-json-mapper will dump the concrete object type to the JSON output during serialization process. This ensures, that dart-json-mapper will be able to reconstruct the object with the proper type during deserialization process.

@jsonSerializable
@Json(typeNameProperty: 'typeName')
abstract class Business {
  String name;
}

@jsonSerializable
class Hotel extends Business {
  int stars;

  Hotel(this.stars);
}

@jsonSerializable
class Startup extends Business {
  int userCount;

  Startup(this.userCount);
}

@jsonSerializable
class Stakeholder {
  String fullName;
  List<Business> businesses = [];

  Stakeholder(this.fullName, this.businesses);
}

// given
final jack = Stakeholder("Jack", [Startup(10), Hotel(4)]);

// when
final String json = JsonMapper.serialize(jack);
final Stakeholder target = JsonMapper.deserialize(json);

// then
expect(target.businesses[0], TypeMatcher<Startup>());
expect(target.businesses[1], TypeMatcher<Hotel>());

Serialization template

In case you already have an instance of huge JSON Map object and portion of it needs to be surgically updated, then you can pass your Map<String, dynamic> instance as a template parameter for SerializationOptions

enum Color { Red, Blue, Green, Brown, Yellow, Black, White }

// given
final template = {'a': 'a', 'b': true};

// when
final json = JsonMapper.serialize(Car('Tesla S3', Color.Black),
  SerializationOptions(indent: '', template: template));

// then
expect(json,
  '''{"a":"a","b":true,"modelName":"Tesla S3","color":"Color.Black"}''');

Deserialization template

In case you need to deserialize specific Map<K, V> type then you can pass typed instance of it as a template parameter for DeserializationOptions.

Since typed Map<K, V> instance cannot be created dynamically due to Dart language nature, so you are providing ready made instance to use for deserialization output.

enum Color { Red, Blue, Green, Brown, Yellow, Black, White }

// given
final json = '{"Color.Black":1,"Color.Blue":2}';

// when
final target = JsonMapper.deserialize(
          json, DeserializationOptions(template: <Color, int>{}));

// then
expect(target, TypeMatcher<Map<Color, int>>());
expect(target.containsKey(Color.Black), true);
expect(target.containsKey(Color.Blue), true);
expect(target[Color.Black], 1);
expect(target[Color.Blue], 2);

Name casing styles [Pascal, Kebab, Snake, SnakeAllCaps]

Assuming your Dart code is following Camel case style, but that is not always true for JSON models, they could follow one of those popular - Pascal, Kebab, Snake, SnakeAllCaps styles, right?

That's why we need a smart way to manage that, instead of hand coding each property using @JsonProperty(name: ...) it is possible to pass CaseStyle parameter to serialization / deserialization methods.

@jsonSerializable
class NameCaseObject {
  String mainTitle;
  String description;
  bool hasMainProperty;

  NameCaseObject({this.mainTitle, this.description, this.hasMainProperty});
}

/// Serialization

// given
final instance = NameCaseObject(
    mainTitle: 'title', description: 'desc', hasMainProperty: true);
// when
final json = JsonMapper.serialize(instance,
    SerializationOptions(indent: '', caseStyle: CaseStyle.Kebab));
// then
expect(json, '''{"main-title":"title","description":"desc","has-main-property":true}''');

/// Deserialization

// given
final json = '''{"main-title":"title","description":"desc","has-main-property":true}''';
// when
final instance = JsonMapper.deserialize<NameCaseObject>(
    json, DeserializationOptions(caseStyle: CaseStyle.Kebab));
// then
expect(instance.mainTitle, 'title');
expect(instance.description, 'desc');
expect(instance.hasMainProperty, true);

Nesting configuration

In case if you need to operate on particular portions of huge JSON object and you don't have a true desire to reconstruct the same deep nested JSON objects hierarchy with corresponding Dart classes. This section is for you!

Say, you have a json similar to this one

{
  "root": {
    "foo": {
      "bar": {
        "baz": {
          "items": [
            "a",
            "b",
            "c"
          ]
        }
      }
    }
  }
}          

And with code similar to this one

@jsonSerializable
@Json(name: 'root/foo/bar')
class BarObject {
  @JsonProperty(name: 'baz/items')
  List<String> items;

  BarObject({this.items});
}

// when
final instance = JsonMapper.deserialize<BarObject>(json);

// then
expect(instance.items.length, 3);
expect(instance.items, ['a', 'b', 'c']);

you'll have it done nice and quick.

@Json(name: 'root/foo/bar') provides a root nesting for the entire annotated class, this means all class fields will be nested under this 'root/foo/bar' path in Json.

@JsonProperty(name: 'baz/items') provides a field nesting relative to the class root nesting

name is compliant with RFC 6901 JSON pointer

Schemes

Scheme - is a set of annotations associated with common scheme id. This enables the possibility to map a single Dart class to many different JSON structures.

This approach usually useful for distinguishing [DEV, PROD, TEST, ...] environments, w/o producing separate Dart classes for each environment.

enum Scheme { A, B }

@jsonSerializable
@Json(name: 'default')
@Json(name: '_', scheme: Scheme.B)
@Json(name: 'root', scheme: Scheme.A)
class Object {
  @JsonProperty(name: 'title_test', scheme: Scheme.B)
  String title;

  Object(this.title);
}

// given
final instance = Object('Scheme A');
// when
final json = JsonMapper.serialize(instance, SerializationOptions(indent: '', scheme: Scheme.A));
// then
expect(json, '''{"root":{"title":"Scheme A"}}''');

// given
final instance = Object('Scheme B');
// when
final json = JsonMapper.serialize(instance, SerializationOptions(indent: '', scheme: Scheme.B));
// then
expect(json, '''{"_":{"title_test":"Scheme B"}}''');

// given
final instance = Object('No Scheme');
// when
final json = JsonMapper.serialize(instance, SerializationOptions(indent: ''));
// then
expect(json, '''{"default":{"title":"No Scheme"}}''');

Objects cloning

If you are wondering how to deep-clone Dart Objects, or even considering using libraries like Freezed to accomplish that, then this section probably will be useful for you

// given
final car = Car('Tesla S3', Color.Black);

// when
final cloneCar = JsonMapper.clone(car);

// then
expect(cloneCar == car, false);
expect(cloneCar.color == car.color, true);
expect(cloneCar.model == car.model, true);

Or if you would like to override some properties for the clonned object instance

// given
final car = Car('Tesla S3', Color.Black);

// when
final cloneCar = JsonMapper.copyWith(car, {'color': Color.Blue}); // overriding Black by Blue

// then
expect(cloneCar == car, false);
expect(cloneCar.color, Color.Blue);
expect(cloneCar.model, car.model);

Custom types

For the very custom types, specific ones, or doesn't currently supported by this library, you can provide your own custom Converter class per each custom runtimeType.

/// Abstract class for custom converters implementations
abstract class ICustomConverter<T> {
  dynamic toJSON(T object, [JsonProperty jsonProperty]);
  T fromJSON(dynamic jsonValue, [JsonProperty jsonProperty]);
}

All you need to get going with this, is to implement this abstract class

class CustomStringConverter implements ICustomConverter<String> {
  const CustomStringConverter() : super();

  @override
  String fromJSON(dynamic jsonValue, [JsonProperty jsonProperty]) {
    return jsonValue;
  }

  @override
  dynamic toJSON(String object, [JsonProperty jsonProperty]) {
    return '_${object}_';
  }
}

And register it afterwards, if you want to have it applied for all occurrences of specified type

JsonMapper().useAdapter(JsonMapperAdapter(
  converters: {
    String: CustomStringConverter()
  })
);

OR use it individually on selected class fields, via @JsonProperty annotation

@JsonProperty(converter: CustomStringConverter())
String title;

Annotations

  • @JsonSerializable() or @jsonSerializable for short, It's a required marker annotation for class or Enum declarations. Use it to mark all the Dart objects you'd like to be traveling to / from JSON
    • Has NO params
  • @JsonConstructor() or @jsonConstructor for short, It's an optional constructor only marker annotation. Use it to mark specific Dart class constructor you'd like to be used during deserialization.
    • scheme dynamic Scheme marker to associate this meta information with particular mapping scheme
  • @Json(...) It's an optional annotation for class or Enum declaration, describes a Dart object to JSON Object mapping. Why it's not a @JsonObject()? just for you to type less characters 😄
    • name Defines RFC 6901 JSON pointer, denotes the json Object root name/path to be used for mapping. Example: 'foo', 'bar', 'foo/bar/baz'
    • caseStyle The most popular ways to combine words into a single string. Based on assumption: That all Dart class fields initially given as CaseStyle.Camel
    • typeNameProperty declares the necessity for annotated class and all it's subclasses to dump their own type name to the property named as this param value
    • enumValues Provides a way to specify enum values, via Dart built in capability for all Enum instances. Enum.values
    • valueDecorators Provides an inline way to specify a static function which will return a Map of value decorators, to support type casting activities for Map<K, V>, and other generic Iterables instead of global adapter approach
    • ignoreNullMembers If set to true Null class members will be excluded from serialization process
    • allowCircularReferences As of int type. Allows certain number of circular object references during serialization.
    • scheme dynamic Scheme marker to associate this meta information with particular mapping scheme
  • @JsonProperty(...) It's an optional class member annotation, describes JSON Object property mapping.
    • name Defines RFC 6901 JSON pointer, denotes the name/path to be used for property mapping relative to the class root nesting Example: 'foo', 'bar', 'foo/bar/baz'
    • scheme dynamic Scheme marker to associate this meta information with particular mapping scheme
    • converter Declares custom converter instance, to be used for annotated field serialization / deserialization
    • converterParams A Map<String, dynamic> of named parameters to be passed to the converter instance
    • ignore A bool declares annotated field as ignored so it will be excluded from serialization / deserialization process
    • ignoreIfNull A bool declares annotated field as ignored if it's value is null so it will be excluded from serialization / deserialization process
    • enumValues Provides a way to specify enum values, via Dart built in capability for all Enum instances. Enum.values
    • defaultValue Defines field default value

Known limitations

  • Dart code obfuscation. If you are using or planning to use extra-gen-snapshot-options=--obfuscate option with your Flutter project, this library shouldn't be your primary choice then. At the moment there is no workaround for this to play nicely together.

Complementary adapter libraries

If you want a seamless integration with popular use cases, feel free to pick an existing adapter or create one for your use case and make a PR to this repo.

Adapter - is a library which contains a bundle of pre-configured:

For example, you would like to refer to Color type from Flutter in your model class.

  • Make sure you have following dependencies in your pubspec.yaml:

    dependencies:
      dart_json_mapper:
      dart_json_mapper_flutter:
    dev_dependencies:
      build_runner:
  • Usually, adapter library exposes final adapter definition instance, to be provided as a parameter to JsonMapper().useAdapter(adapter)

    import 'dart:ui' show Color;
    import 'package:dart_json_mapper/dart_json_mapper.dart' show JsonMapper, jsonSerializable;    
    import 'package:dart_json_mapper_flutter/dart_json_mapper_flutter.dart' show flutterAdapter;
    
    import 'main.reflectable.dart' show initializeReflectable;
    
    @jsonSerializable
    class ColorfulItem {
      String name;
      Color color;
    
      ColorfulItem(this.name, this.color);
    }
    
    void main() {
      initializeReflectable();
      JsonMapper().useAdapter(flutterAdapter);
      
      print(JsonMapper.serialize(
         ColorfulItem('Item 1', Color(0x003f4f5f))
      ));
    }

    output:

    {
      "name": "Item 1",
      "color": "#003F4F5F"
    }

You can easily mix and combine several adapters using following one-liner:

JsonMapper()
   .useAdapter(fixnumAdapter)
   .useAdapter(flutterAdapter)
   .useAdapter(mobXAdapter)
   .info(); // print out a list of used adapters to console