json_serializable.dart
json_serializable.dart copied to clipboard
Use dart json_serializable to map an object field to JSON flat keys (one to many mapping)
I have a dart object that includes a field of type Money
, which itself is composed of amount
and currency
:
@JsonSerializable()
class Account {
final String id;
final String type;
final String subtype;
final String origin;
final String name;
final String status;
final String currency;
final Money balance; <== value object
...
}
Money
looks something like this:
class Money {
final int amount;
final String currency;
const Money(this.amount, this.currency);
...
}
The above is to be mapped for use by sqflite
, therefore the target JSON must be a flat JSON, like:
{
"id": String,
"type": String,
"subtype": String,
"origin": String,
"name": String,
"status": String,
"currency": String,
"balanceAmount": int; <== value object
"balanceCurrency": String; <== value object
}
I understand I can use JsonKey.readValue
to extract a composite object from the entire JSON object prior to decoding.
But how can I do the opposite? Grab the two values from the Money
instance and map them to balanceAmount
and balanceCurrency
?
Nothing in the search I did on json_serializable
api docs, GitHub issues, or StackOverflow appears to respond to this in particular: how to map one field to two (or more) target keys?
Maybe json_seriazable
doesn't have this functionality yet for now. So in manual approach, it could be like this
@JsonSeriazable()
class Account {
// properties...
factory Account.fromJson(Map<String, dynamic> json) => _$AccountFromJson(json);
Map<String, dynamic> toJson() => _$AccountToJson(this);
// add this method
Map<String, dynamic> toFlatJson() {
final currentJson = toJson();
final newJson = {...currentJson};
// flattening 'balance' value
currentJson['balance']!.forEach((k, v) {
newJson[k] = v;
});
// remove 'balance' value from newJson
newJson.remove('balance');
return newJson;
}
}
Note:
Well, I didn't test this yet. So you might manipulate this code snippet based on your own problems.
At the moment I also have not found any other way than to use readValue
and change the toJson
method myself. I'll show how this can be done using freezed
:
|-- freezed 2.5.2
| |-- source_gen 1.5.0
|-- freezed_annotation 2.4.1
|-- json_annotation 4.9.0
|-- json_serializable 6.8.0
The models look like this (Pay attention to Account.toJson
and the passJson
function, which simply passes raw json):
Object passJson(Map json, _) => json;
@freezed
class Account with _$Account {
@JsonSerializable(explicitToJson: true)
const factory Account({
@JsonKey(name: 'status') required String status,
@JsonKey(name: '_balance', readValue: passJson) required Money balance,
}) = _Account;
factory Account.fromJson(Map<String, dynamic> json) =>
_$AccountFromJson(json);
const Account._();
@override
Map<String, dynamic> toJson() => toJson()
..addAll(balance.toJson())
..remove('_balance');
Map<String, dynamic> toFlatJson() => toJson()
..addAll(balance.toJson())
..remove('_balance');
}
@freezed
class Money with _$Money {
const factory Money({
@JsonKey(name: 'balanceCurrency') required String currency,
@JsonKey(name: 'balanceAmount') required int amount,
}) = _Money;
factory Money.fromJson(Map<String, dynamic> json) => _$MoneyFromJson(json);
}
Then the generated files look like this:
_$AccountImpl _$$AccountImplFromJson(Map<String, dynamic> json) =>
_$AccountImpl(
status: json['status'] as String,
balance:
Money.fromJson(passJson(json, '_balance') as Map<String, dynamic>),
);
Map<String, dynamic> _$$AccountImplToJson(_$AccountImpl instance) =>
<String, dynamic>{
'status': instance.status,
'_balance': instance.balance.toJson(),
};
_$MoneyImpl _$$MoneyImplFromJson(Map<String, dynamic> json) => _$MoneyImpl(
currency: json['balanceCurrency'] as String,
amount: (json['balanceAmount'] as num).toInt(),
);
Map<String, dynamic> _$$MoneyImplToJson(_$MoneyImpl instance) =>
<String, dynamic>{
'balanceCurrency': instance.currency,
'balanceAmount': instance.amount,
};
This is still not as convenient as some kind of “magic short annotation”, but it allows you to complete the task using flat json.
Update
Unfortunately, the method above doesn't work because the toJson
method doesn't work as expected (the overridden toJson
method does not run our code):
So I added const Account._();
to the example above and defined toFlatJson
. Then:
main() {
final json = {
'status': 'receipted',
'balanceCurrency': '1.0',
'balanceAmount': 500,
};
final account = Account.fromJson(json);
print(account.toJson()); // {status: receipted, _balance: {balanceCurrency: 1.0, balanceAmount: 500}}
print(account.toFlatJson()); // {status: receipted, balanceCurrency: 1.0, balanceAmount: 500}
}