はじめに
データの一覧を取得するAPIで、配列ではなく連番のキーを用いたオブジェクトで返されるといった場面がありました。
私はFreezed
とRetrofit
をもちいてAPIレスポンスの自動パースをしています。
MapオブジェクトからListに直接パースしたかったため、コンバーターを作成しました。
問題の状況
以下のような形式でレスポンスが返されます。
{
"dataList": {
"1": {"id": 1, "name": "Item 1"},
"2": {"id": 2, "name": "Item 2"},
...
}
}
dart側では以下のようなモデルをListで受け取る想定です。
@freezed
abstract class Model with _$Model {
const factory Model({
required int id,
required String name,
}) = _Model;
factory Model.fromJson(Map<String, dynamic> json) =>
_$ModelFromJson(json);
}
解決方法
無理やり変換するコンバーターを作成します
class DateListConverter
implements JsonConverter<List<Model>, Map<String, dynamic>> {
const DateListConverter();
@override
List<Model> fromJson(Map<String, dynamic> jsonMap) {
// mapから要素を取り出す
return jsonMap.values.map((value) {
if (value is Map<String, dynamic>) {
// modelのfromJson
return Model.fromJson(value);
}
throw FormatException(
'Expected a Map<String, dynamic> for value, but got ${value.runtimeType}',
);
}).toList(); // List化
}
@override
Map<String, dynamic> toJson(List<Model> dataList) {
final jsonMap = <String, dynamic>{};
for (var i = 0; i < dataList.length; i++) {
final item = dataList[i];
// keyを指定
final key = i;
jsonMap[key] = toJsonT(item);
}
return jsonMap;
}
}
以上のコンバーターを用いてレスポンスモデルを作ります。
@freezed
abstract class DataListResponseModel with _$DataListResponseModel {
const factory DataListResponseModel({
@DataListConverter() required List<Model> dataList,
}) = _DataListResponseModel;
factory DataListResponseModel.fromJson(Map<String, dynamic> json) =>
_$DataListResponseModelFromJson(json);
}
このレスポンスモデルを用いてAPI通信メソッドを作ります(Retrofit)
@RestApi()
abstract class DataApiService {
factory DataApiService(Dio dio) = _DataApiService;
@GET('/data/list')
Future<DataResponseModel> getDataList();
}
あとはレスポンスモデルからdataListを取り出せば完了です!
ベースクラスを作成して汎用化
以下のようにabstract classを用いて継承できるようにすると記述量を減らして使い回しやすくなります。
abstract class MapToListConverter<T>
implements JsonConverter<List<T>, Map<String, dynamic>> {
const MapToListConverter();
/// Listに格納するオブジェクト(T)のfromJson
T fromJsonT(Map<String, dynamic> json);
/// Listに格納するオブジェクト(T)のtoJson
Map<String, dynamic> toJsonT(T object);
/// toJsonの際に使用するキーを決定する関数
String keySelector(T object, int index);
@override
List<T> fromJson(Map<String, dynamic> jsonMap) {
return jsonMap.values.map((value) {
if (value is Map<String, dynamic>) {
return fromJsonT(value);
}
throw FormatException(
'Expected a Map<String, dynamic> for value, but got ${value.runtimeType}',
);
}).toList();
}
@override
Map<String, dynamic> toJson(List<T> objectList) {
final jsonMap = <String, dynamic>{};
for (var i = 0; i < objectList.length; i++) {
final item = objectList[i];
final key = keySelector(item, i);
jsonMap[key] = toJsonT(item);
}
return jsonMap;
}
}
class DataListConverter extends MapToListConverter<Model> {
const DataListConverter();
@override
Model fromJsonT(Map<String, dynamic> json) =>
Model.fromJson(json);
@override
Map<String, dynamic> toJsonT(Model object) => object.toJson();
@override
String keySelector(Model object, int index) => index.toString();
}
おわりに
特殊なケースかもしれませんがretrofitの自動パースを活かしたかったため、このような対応を取りました。
もっと簡易的な方法があればコメントいただけるとありがたいです!