JSON 직렬화 — json_serializable과 freezed
JSON 직렬화 — json_serializable과 freezed
API 응답 JSON을 Dart 객체로 변환하는 작업은 매일 합니다. 수동으로 fromJson/toJson을 작성하면 실수가 잦으니, 코드 생성 도구를 활용하는 것이 실무 표준입니다.
수동 직렬화의 문제
// 수동으로 작성 — 필드가 많아지면 실수하기 쉬움
class User {
final int id;
final String name;
final String email;
const User({required this.id, required this.name, required this.email});
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'] as int, // 타입 캐스팅 실수 가능
name: json['name'] as String, // 키 오타 가능
email: json['email'] as String, // nullable 처리 누락 가능
);
}
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'email': email,
};
}
필드가 10개, 20개가 되면 수동 작성은 비현실적입니다.
json_serializable
설치
dependencies:
json_annotation: ^4.9.0
dev_dependencies:
json_serializable: ^6.8.0
build_runner: ^2.4.0
모델 정의
import 'package:json_annotation/json_annotation.dart';
// 생성될 파일 이름
part 'user.g.dart';
@JsonSerializable()
class User {
final int id;
final String name;
final String email;
// JSON 키 이름이 다를 때
@JsonKey(name: 'created_at')
final DateTime createdAt;
// null 허용
@JsonKey(name: 'profile_image')
final String? profileImage;
// 기본값 지정
@JsonKey(defaultValue: false)
final bool isActive;
const User({
required this.id,
required this.name,
required this.email,
required this.createdAt,
this.profileImage,
this.isActive = false,
});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
코드 생성
# 1회 실행
dart run build_runner build
# 파일 변경 감지 (개발 중 권장)
dart run build_runner watch
# 충돌 시 삭제 후 재생성
dart run build_runner build --delete-conflicting-outputs
중첩 객체
@JsonSerializable(explicitToJson: true) // 중첩 객체도 toJson 적용
class Post {
final int id;
final String title;
final User author; // 중첩 객체
final List<Comment> comments; // 중첩 리스트
const Post({
required this.id,
required this.title,
required this.author,
required this.comments,
});
factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);
Map<String, dynamic> toJson() => _$PostToJson(this);
}
freezed — 불변 모델 + JSON 직렬화
freezed는 불변 데이터 클래스에 copyWith, ==, hashCode, toString, JSON 직렬화를 자동 생성합니다.
설치
dependencies:
freezed_annotation: ^2.4.0
json_annotation: ^4.9.0
dev_dependencies:
freezed: ^2.5.0
json_serializable: ^6.8.0
build_runner: ^2.4.0
모델 정의
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
const factory User({
required int id,
required String name,
required String email,
@JsonKey(name: 'created_at') required DateTime createdAt,
@JsonKey(name: 'profile_image') String? profileImage,
@Default(false) bool isActive,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
freezed가 생성해주는 것들
final user = User(
id: 1,
name: '심정훈',
email: 'test@email.com',
createdAt: DateTime.now(),
);
// copyWith (불변 객체 복사)
final updatedUser = user.copyWith(name: '정훈');
// == 비교 (값 기반)
print(user == user.copyWith()); // true
// toString
print(user); // User(id: 1, name: 심정훈, ...)
// JSON 직렬화
final json = user.toJson();
final fromJson = User.fromJson(json);
freezed의 Union Types
API 응답이나 상태 관리에서 매우 유용합니다.
@freezed
class Result<T> with _$Result<T> {
const factory Result.success(T data) = Success<T>;
const factory Result.failure(String message) = Failure<T>;
const factory Result.loading() = Loading<T>;
}
// 패턴 매칭으로 사용
Widget buildResult(Result<User> result) {
return result.when(
success: (user) => Text(user.name),
failure: (message) => Text('에러: $message'),
loading: () => const CircularProgressIndicator(),
);
}
// map도 가능 (타입 유지)
result.map(
success: (success) => Text(success.data.name),
failure: (failure) => Text(failure.message),
loading: (_) => const CircularProgressIndicator(),
);
실전 패턴: API 응답 모델
@freezed
class ApiResponse<T> with _$ApiResponse<T> {
const factory ApiResponse({
required int status,
required String message,
T? data,
}) = _ApiResponse<T>;
factory ApiResponse.fromJson(
Map<String, dynamic> json,
T Function(Object?) fromJsonT,
) => _$ApiResponseFromJson(json, fromJsonT);
}
@freezed
class PaginatedResponse<T> with _$PaginatedResponse<T> {
const factory PaginatedResponse({
required List<T> items,
required int totalCount,
required int page,
required int pageSize,
}) = _PaginatedResponse<T>;
}
json_serializable vs freezed 비교
| 기능 | json_serializable | freezed |
|---|---|---|
| fromJson/toJson | O | O |
| copyWith | X | O |
| == / hashCode | X | O |
| toString | X | O |
| Union types | X | O |
| 보일러플레이트 | 적음 | 매우 적음 |
| 빌드 시간 | 빠름 | 느림 |
면접 포인트: "freezed를 왜 사용하나요?"에 대한 답은 **불변성 **, ** 값 비교 **, copyWith, Union 타입 지원입니다. 상태 관리에서 상태 객체를 불변으로 관리하는 것이 버그를 줄이는 핵심입니다.
정리
- 수동 JSON 직렬화는 실수가 잦으니, 코드 생성 도구를 사용하세요
json_serializable은 JSON 변환만 필요할 때 가볍게 사용합니다freezed는 불변 데이터 클래스 + JSON + Union 타입을 한 번에 지원합니다@JsonKey로 JSON 키 이름 매핑, 기본값, null 처리를 할 수 있습니다dart run build_runner watch로 코드를 자동 생성하면 편합니다
댓글 로딩 중...