[Flutter] json_serializable 사용하기
json_serializable | Dart package
json serializable 은 json 직렬화 역직렬화 도구입니다.
자동 코드 생성:json_serializable을 사용하면 JSON 직렬화/역직렬화를 위한 boilerplate 코드를 자동으로 생성할 수 있습니다.
이를 통해 개발자가 직접 fromJson(), toJson() 메서드를 작성할 필요가 없어집니다.
간편한 사용:클래스에 @JsonSerializable() 어노테이션을 추가하면 json_serializable이 자동으로 필요한 코드를 생성합니다. 이를 통해 JSON 처리 로직을 쉽게 구현할 수 있습니다.
유연한 설정:JsonSerializable 어노테이션에는 다양한 옵션을 설정할 수 있습니다. 예를 들어 필드 이름 매핑, 기본값 설정, 필수 필드 지정 등을 할 수 있습니다.확장성:json_serializable은 다양한 JSON 처리 라이브러리와 호환되므로, 프로젝트 요구사항에 따라 다른 라이브러리와 함께 사용할 수 있습니다.
생산성 향상:자동 코드 생성 기능으로 인해 개발자가 직접 작성해야 하는 코드량이 줄어들어 생산성이 향상됩니다.
요약하면, json_serializable은 플러터 앱 개발 시 JSON 데이터 처리를 간편하게 할 수 있도록 도와주는 강력한 도구입니다.
이를 통해 개발자는 보일러플레이트 코드 작성에 시간을 낭비하지 않고, 비즈니스 로직에 집중할 수 있습니다.
한번 사용해봅시다.
아래와 같은 json을 받아온다고 했을때
{
"meta": {
"count": 20,
"hasMore": true
},
"data": [
{
"id": "1952a209-7c26-4f50-bc65-086f6e64dbbd",
"name": "우라나라에서 가장 맛있는 짜장면집",
"thumbUrl": "/img/thumb.png",
"tags": [
"신규",
"세일중"
],
"priceRange": "cheap",
"ratings": 4.89,
"ratingsCount": 200,
"deliveryTime": 20,
"deliveryFee": 3000
}
]
}
repository.dart
import 'package:json_annotation/json_annotation.dart';
part 'restaurant_model.g.dart';
enum RestaurantPriceRange {
expensive,
medium,
cheap,
}
@JsonSerializable()
class RestaurantModel {
final String id;
final String name;
@JsonKey(
fromJson: DataUtils.pathToUrl,
)
final String thumbUrl;
final List<String> tags;
final RestaurantPriceRange priceRange;
final double ratings;
final int ratingsCount;
final int deliveryTime;
final int deliveryFee;
RestaurantModel({
required this.id,
required this.name,
required this.thumbUrl,
required this.tags,
required this.priceRange,
required this.ratings,
required this.ratingsCount,
required this.deliveryTime,
required this.deliveryFee,
});
factory RestaurantModel.fromJson(Map<String, dynamic> json)
=> _$RestaurantModelFromJson(json);
Map<String, dynamic> toJson() => _$RestaurantModelToJson(this);
}
part 'restaurant_model.g.dart';라는 파일에 @JsonSerializable() 선언한 클래스를 기반으로 직렬화 코드를 작성해 줍니다.
코드를 살펴보면 json형태에 맞게 자료형을 final로 선언하고 필수적으로 값이 들어와야함을 설정해주어 누락을 방지합니다.
@JsonKey의 경우 이미지 url을 변환해서 할당해주기때문애 따로 함수를 설정해 변환해 줍니다.
RestaurantPriceRange의 경우 3가지 선택지만 있기에 enum 으로 만들어 선언합니다.
바탕으로 만들어진 코드입니다.
restaurant_model.g.dart
RestaurantModel _$RestaurantModelFromJson(Map<String, dynamic> json) =>
RestaurantModel(
id: json['id'] as String,
name: json['name'] as String,
thumbUrl: DataUtils.pathToUrl(json['thumbUrl'] as String),
tags: (json['tags'] as List<dynamic>).map((e) => e as String).toList(),
priceRange:
$enumDecode(_$RestaurantPriceRangeEnumMap, json['priceRange']),
ratings: (json['ratings'] as num).toDouble(),
ratingsCount: json['ratingsCount'] as int,
deliveryTime: json['deliveryTime'] as int,
deliveryFee: json['deliveryFee'] as int,
);
Map<String, dynamic> _$RestaurantModelToJson(RestaurantModel instance) =>
<String, dynamic>{
'id': instance.id,
'name': instance.name,
'thumbUrl': instance.thumbUrl,
'tags': instance.tags,
'priceRange': _$RestaurantPriceRangeEnumMap[instance.priceRange]!,
'ratings': instance.ratings,
'ratingsCount': instance.ratingsCount,
'deliveryTime': instance.deliveryTime,
'deliveryFee': instance.deliveryFee,
};
const _$RestaurantPriceRangeEnumMap = {
RestaurantPriceRange.expensive: 'expensive',
RestaurantPriceRange.medium: 'medium',
RestaurantPriceRange.cheap: 'cheap',
};
이렇게 작성된 코드는 RestApi 통신에서 아래와같이 사용됩니다.
@override
Future<CursorPagination<RestaurantModel>> paginate() async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _headers = <String, dynamic>{r'accessToken': 'true'};
_headers.removeWhere((k, v) => v == null);
final _data = <String, dynamic>{};
final _result = await _dio.fetch<Map<String, dynamic>>(
_setStreamType<CursorPagination<RestaurantModel>>(
Options(method: 'GET', headers: _headers, extra: _extra)
.compose(_dio.options, '/',
queryParameters: queryParameters, data: _data)
.copyWith(baseUrl: baseUrl ?? _dio.options.baseUrl)));
final value = CursorPagination<RestaurantModel>.fromJson(
_result.data!,
(json) => RestaurantModel.fromJson(json as Map<String, dynamic>),
);
return value;
}
번외
일부가 동일한 모델링을 구현할때의 방법입니다.
예를 들어 레스토랑 상세페이지에는 1.레스토랑 모델, 2.레스토랑 메뉴정보가 들어가야합니다.
이때 통신을 통해서는 이전에 구현한 1. 레스토랑 모델과 2. 레스토랑 메뉴정보가 들어갑니다.
이때 방법은 이전에 모델링한 모델을 extends해서 만드는 것입니다.
import 'package:json_annotation/json_annotation.dart';
part 'restaurant_detail_model.g.dart';
@JsonSerializable()
class RestaurantDetailModel extends RestaurantModel {
final String detail;
final List<RestaurantProductModel> products;
// 속성중에 클래스가 있으면 그 속성도 json으로부터 받아와서 전환하는 과정이 필요하다.
RestaurantDetailModel({
required super.id,
required super.name,
required super.thumbUrl,
required super.tags,
required super.priceRange,
required super.ratings,
required super.ratingsCount,
required super.deliveryTime,
required super.deliveryFee,
required this.detail,
required this.products,
});
factory RestaurantDetailModel.fromJson(Map<String, dynamic> json) =>
_$RestaurantDetailModelFromJson(json);
}
@JsonSerializable()
class RestaurantProductModel {
final String id;
final String name;
@JsonKey(
fromJson: DataUtils.pathToUrl,
)
final String imgUrl;
final String detail;
final int price;
RestaurantProductModel({
required this.id,
required this.name,
required this.imgUrl,
required this.detail,
required this.price,
});
factory RestaurantProductModel.fromJson(Map<String, dynamic> json) =>
_$RestaurantProductModelFromJson(json);
}
JsonSerializable을 두군데에 합니다.
- 레스토랑모델 + 메뉴모델
- 메뉴모델
1.에는 상속을 받아서 사용하기때문애 final로 재사용할필요가 없습니다.
유의할점은 메뉴모델도 리스트라는 점입니다.