JSON to Dart
fromJson 및 toJson이 포함된 Dart 클래스를 JSON에서 생성
JSON 입력
Dart 출력
JSON to Dart 변환이란?
JSON to Dart 변환은 원시 JSON 객체를 받아 타입이 지정된 필드, 명명된 생성자, fromJson 팩토리, toJson 메서드가 포함된 Dart 클래스 정의를 생성합니다. Dart는 Flutter에서 런타임 리플렉션을 지원하지 않습니다(dart:mirrors가 비활성화되어 있음). 따라서 명시적인 매핑 코드를 작성하지 않으면 JSON을 타입이 지정된 객체로 역직렬화할 수 없습니다. 모든 REST API 응답, Firebase 문서, 설정 페이로드는 타입 안전하게 필드에 접근하려면 대응하는 Dart 모델 클래스가 필요합니다.
JSON을 위한 일반적인 Dart 모델 클래스는 각 키에 대한 final 필드, 명명된 매개변수가 있는 생성자(nullable이 아닌 필드에는 required 키워드 사용), String에서 dynamic으로의 Map을 읽는 fromJson 팩토리 생성자, String에서 dynamic으로의 Map을 반환하는 toJson 메서드로 구성됩니다. 중첩된 JSON 객체는 별도의 클래스가 됩니다. 배열은 타입이 지정된 List 필드가 됩니다. nullable JSON 값은 타입에 ? 접미사를 붙이는 Dart의 null 안전성 문법을 사용합니다.
이러한 모델 클래스를 직접 작성하려면 각 JSON 키를 읽고, Dart 타입을 결정하고, 각 필드에 대한 fromJson 캐스트를 생성하고(.map().toList()를 사용한 리스트 매핑 포함), toJson 맵 리터럴을 구성하고, 중첩 객체마다 이 과정을 반복해야 합니다. 12개의 필드와 2개의 중첩 객체가 있는 JSON 객체라면 3개의 클래스, 6개의 팩토리 라인, 수십 개의 캐스트 표현식이 필요합니다. 변환기는 이 모든 것을 한 번의 붙여넣기로 수 밀리초 안에 처리합니다.
JSON to Dart 변환기를 사용하는 이유
JSON에서 Dart 모델 클래스를 수동으로 작성하려면 필드 이름을 확인하고, 샘플 값에서 타입을 추론하고, 올바른 null 처리로 fromJson 캐스트를 작성하고, 중첩 객체마다 이 과정을 반복해야 합니다. API 구조가 변경되면 모든 필드 업데이트가 생성자, fromJson, toJson을 모두 건드립니다. 변환기는 이러한 반복 작업을 없애줍니다.
JSON to Dart 활용 사례
JSON to Dart 타입 매핑
모든 JSON 값은 특정 Dart 타입에 매핑됩니다. 아래 표는 변환기가 각 JSON 타입을 어떻게 변환하는지 보여줍니다. 대안 열은 덜 일반적이거나 수동 매핑 시나리오에서 사용되는 타입을 보여줍니다.
| JSON 타입 | 예시 값 | Dart 타입 | 대안 |
|---|---|---|---|
| string | "hello" | String | String |
| number (integer) | 42 | int | int |
| number (float) | 3.14 | double | double |
| boolean | true | bool | bool |
| null | null | dynamic | Null / dynamic |
| object | {"k": "v"} | NestedClass | Map<String, dynamic> |
| array of strings | ["a", "b"] | List<String> | List<String> |
| array of objects | [{"id": 1}] | List<Item> | List<Item> |
| mixed array | [1, "a"] | List<dynamic> | List<dynamic> |
Dart JSON 직렬화 방식
Dart와 Flutter는 JSON 직렬화를 처리하는 여러 방법을 제공합니다. 수동 fromJson/toJson은 가장 단순한 방식으로 코드 생성이 필요 없습니다. 대규모 프로젝트에서는 json_serializable, freezed 같은 build_runner 기반 솔루션이 컴파일 시점에 보일러플레이트를 생성하여 오류를 줄이고 모델을 JSON 계약과 일관성 있게 유지합니다.
| 방식 | 설명 | 출처 |
|---|---|---|
| json_serializable | Code-generation-based JSON serialization. Generates .g.dart files at build time via build_runner. Type-safe and compile-time verified. | Official (Google) |
| freezed | Generates immutable data classes with copyWith, fromJson/toJson, equality, and pattern matching support. Built on top of json_serializable. | Community (rrousselGit) |
| built_value | Generates immutable value types with serialization. Enforces immutability patterns. Used in larger codebases with strict data modeling. | |
| dart_mappable | Annotation-based mapper that generates fromJson, toJson, copyWith, and equality. Simpler setup than freezed with similar features. | Community |
| Manual fromJson/toJson | Hand-written factory constructors and toJson methods. No code generation needed. Full control over the mapping logic. | Built-in Dart |
수동 vs json_serializable vs freezed
Dart에는 JSON 모델 클래스를 위한 세 가지 일반적인 방식이 있습니다. 수동 fromJson/toJson은 의존성이 없습니다. json_serializable은 매핑 코드를 자동화합니다. freezed는 json_serializable 위에 불변성, copyWith, 패턴 매칭을 추가합니다.
코드 예시
이 예시들은 JSON 역직렬화에 생성된 Dart 클래스를 사용하는 방법, build_runner로 json_serializable을 설정하는 방법, JavaScript와 Python에서 프로그래밍 방식으로 Dart 클래스를 생성하는 방법을 보여줍니다.
import 'dart:convert';
class User {
final int id;
final String name;
final String email;
final bool active;
final Address address;
final List<String> tags;
User({
required this.id,
required this.name,
required this.email,
required this.active,
required this.address,
required this.tags,
});
factory User.fromJson(Map<String, dynamic> json) => User(
id: json['id'] as int,
name: json['name'] as String,
email: json['email'] as String,
active: json['active'] as bool,
address: Address.fromJson(json['address'] as Map<String, dynamic>),
tags: (json['tags'] as List<dynamic>).map((e) => e as String).toList(),
);
Map<String, dynamic> toJson() => {
'id': id,
'name': name,
'email': email,
'active': active,
'address': address.toJson(),
'tags': tags,
};
}
class Address {
final String street;
final String city;
final String zip;
Address({required this.street, required this.city, required this.zip});
factory Address.fromJson(Map<String, dynamic> json) => Address(
street: json['street'] as String,
city: json['city'] as String,
zip: json['zip'] as String,
);
Map<String, dynamic> toJson() => {'street': street, 'city': city, 'zip': zip};
}
void main() {
final jsonStr = '{"id":1,"name":"Alice","email":"alice@example.com","active":true,"address":{"street":"123 Main","city":"Springfield","zip":"12345"},"tags":["admin","user"]}';
final user = User.fromJson(jsonDecode(jsonStr));
print(user.name); // -> Alice
print(jsonEncode(user.toJson())); // -> round-trip back to JSON
}// pubspec.yaml dependencies:
// json_annotation: ^4.8.0
// dev_dependencies:
// build_runner: ^2.4.0
// json_serializable: ^6.7.0
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart'; // generated by: dart run build_runner build
@JsonSerializable()
class User {
final int id;
final String name;
final String email;
@JsonKey(name: 'is_active')
final bool isActive;
final List<String> tags;
User({
required this.id,
required this.name,
required this.email,
required this.isActive,
required this.tags,
});
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
// Run code generation:
// dart run build_runner build --delete-conflicting-outputsfunction jsonToDart(obj, name = "Root") {
const classes = [];
function infer(val, fieldName) {
if (val === null) return "dynamic";
if (typeof val === "string") return "String";
if (typeof val === "number") return Number.isInteger(val) ? "int" : "double";
if (typeof val === "boolean") return "bool";
if (Array.isArray(val)) {
const first = val.find(v => v !== null);
if (!first) return "List<dynamic>";
return `List<${infer(first, fieldName)}>`;
}
if (typeof val === "object") {
const cls = fieldName.charAt(0).toUpperCase() + fieldName.slice(1);
build(val, cls);
return cls;
}
return "dynamic";
}
function build(obj, cls) {
const fields = Object.entries(obj).map(([k, v]) =>
` final ${infer(v, k)} ${k};`
);
classes.push(`class ${cls} {\n${fields.join("\n")}\n}`);
}
build(obj, name);
return classes.join("\n\n");
}
console.log(jsonToDart({ id: 1, name: "Alice", scores: [98, 85] }, "User"));
// class User {
// final int id;
// final String name;
// final List<int> scores;
// }import json
def json_to_dart(obj: dict, class_name: str = "Root") -> str:
classes = []
def infer(val, name):
if val is None:
return "dynamic"
if isinstance(val, bool):
return "bool"
if isinstance(val, int):
return "int"
if isinstance(val, float):
return "double"
if isinstance(val, str):
return "String"
if isinstance(val, list):
if not val:
return "List<dynamic>"
return f"List<{infer(val[0], name)}>"
if isinstance(val, dict):
cls = name[0].upper() + name[1:]
build(val, cls)
return cls
return "dynamic"
def build(obj, cls):
fields = [f" final {infer(v, k)} {k};" for k, v in obj.items()]
classes.append(f"class {cls} {{\n" + "\n".join(fields) + "\n}")
build(obj, class_name)
return "\n\n".join(classes)
data = json.loads('{"id": 1, "name": "Alice", "active": true}')
print(json_to_dart(data, "User"))
# class User {
# final int id;
# final String name;
# final bool active;
# }