JSON to Dart 변환기는 샘플 JSON을 읽어 일반 모델 클래스를 만듭니다. final 필드, 이름 있는 매개변수 생성자, factory Name.fromJson(Map<String, dynamic> json), Map<String, dynamic> toJson() 메서드가 들어갑니다. 매핑 코드를 명시적 캐스트로 전부 풀어서 쓰기 때문에 코드 생성 단계도 json_serializable 의존성도 필요 없습니다.
같은 위치에 있는 값은 모두 하나의 타입으로 합쳐집니다. 객체 배열이면 모든 원소의 키를 합친 클래스 하나가 만들어지고, 일부 원소에만 있는 키는 required가 아닌 널 허용 필드가 되며 값이 null이면 toJson에서 빠집니다. 루트는 객체와 배열 모두 가능하고, 루트 타입 이름 옵션(기본값 Payload)이 첫 클래스 이름을 정합니다.
변환은 브라우저 안에서 이루어지고 아무것도 업로드되지 않습니다. 입력 한도는 20 MiB입니다. 타입은 붙여넣은 샘플에서 나오므로 샘플 하나로는 키가 항상 있다는 것을 증명할 수 없고, 모든 샘플에서 null인 필드는 타입 정보가 없습니다. 사용하기 전에 널 허용 여부와 숫자 타입을 확인하세요.
사용 방법
JSON 붙여넣기
객체나 배열을 입력창에 붙여넣습니다. 최상위가 문자열, 숫자, 불리언이면 타입 이름을 만들 대상이 없어 거부됩니다.
루트 타입 이름 정하기
Payload가 싫다면 "루트 타입 이름"을 바꿉니다. 영문자와 숫자만 남긴 PascalCase로 바뀌고, 숫자로 시작하면Type접두사가 붙습니다.클래스 확인
루트 클래스가 먼저 나오고 중첩 클래스가 처음 만난 순서대로 이어집니다. 배열 루트라면 클래스들 앞에
typedef Payload = List<PayloadItem>;이 붙습니다.프로젝트에 복사
.dart파일로 저장하고Payload.fromJson(jsonDecode(text) as Map<String, dynamic>)으로 파싱합니다.타입 다듬기
실제 타입을 아는
dynamic은 바꾸고, 샘플에서는 선택 키였지만 API가 항상 보내는 필드는 널 비허용으로 고칩니다.
작동 원리
JSON과 Dart 타입의 대응
도구는 절댓값이 2^63 미만인 정수 표기에 int, 소수점·지수 표기와 소수 값 및 2^63 이상의 정수에 double을 씁니다. 같은 위치에 둘이 섞이면 [1, 1.5]처럼 double로 합쳐집니다. double은 (x as num).toDouble()로 디코딩하므로 서버가 19를 보내도 깨지지 않습니다.
| JSON 값 | Dart 타입 | 비고 |
|---|---|---|
| string | String | |
| true / false | bool | |
| 2^63 미만의 정수 표기 | int | 32비트를 넘어도 int (예: 12345678901) |
| 소수, 또는 정수와 소수가 섞인 값 | double | (x as num).toDouble()로 디코딩 |
| 타입과 null이 함께 있는 값 | T? | 예: String? |
| 모든 샘플에서 null, 또는 한 위치에 서로 다른 모양 | dynamic | 생성자 매개변수는 선택 |
| 객체 | class 이름 | fromJson, toJson이 있는 별도 클래스 |
| 배열 | List<T> | 명시적 .map(...).toList()로 디코딩 |
| 빈 배열 | List<dynamic> | 원소에서 배울 정보가 없음 |
| 일부 객체에만 있는 키 | T? | required가 아니며 null이면 toJson에서 생략 |
| 최상위 배열 | typedef Payload = List<PayloadItem>; | 클래스들보다 앞에 나옴 |
이름과 생성되는 메서드
필드 이름은 camelCase 경계와 영문자·숫자가 아닌 문자에서 나눈 뒤 lowerCamelCase로 이어 붙이므로 first_name은 firstName이 됩니다. 숫자로 시작하면 field가 붙어 field2fa가 되고, class, default, new, late, required 같은 Dart 예약어에는 Value 접미사가 붙어 classValue가 됩니다. 식별자에는 ASCII 영문자와 숫자만 남습니다.
원래 JSON 키는 fromJson과 toJson이 쓰는 문자열 리터럴에 그대로 남아 이름이 바뀌어도 매핑이 깨지지 않습니다. 필드나 클래스 이름이 중복되면 2부터 숫자가 붙습니다. 배열 원소 클래스는 키의 단수형을 따라 이름이 붙어 items는 PayloadItem, categories는 PayloadCategory, data는 PayloadDataItem이 됩니다.
factory Payload.fromJson(Map<String, dynamic> json) => Payload(
firstName: json['first_name'] as String,
tags: (json['tags'] as List<dynamic>).map((e0) => e0 as String).toList(),
);클래스 사용하기
final p = Payload.fromJson(jsonDecode(text) as Map<String, dynamic>);로 객체를 만들고 jsonEncode(p.toJson())으로 다시 직렬화합니다. 널 비허용 필드는 required라서 키가 없으면 캐스트 실패로 디코딩 시점에 예외가 나고, 널 허용 필드는 키가 없거나 null이어도 받아들입니다.
json['id'] as int 같은 캐스트는 엄격해서 샘플에서는 19였는데 서버가 19.5 같은 double을 보내면 예외가 납니다. double 필드는 num을 거치므로 두 경우 모두 안전합니다.
실제 예시
{"id":42,"user":{"name":"Ada","active":true},"roles":["admin","editor"]}class Payload {
final int id;
final PayloadUser user;
final List<String> roles;
Payload({
required this.id,
required this.user,
required this.roles,
});
factory Payload.fromJson(Map<String, dynamic> json) => Payload(
id: json['id'] as int,
user: PayloadUser.fromJson(json['user'] as Map<String, dynamic>),
roles: (json['roles'] as List<dynamic>).map((e0) => e0 as String).toList(),
);
Map<String, dynamic> toJson() => {
'id': id,
'user': user.toJson(),
'roles': roles,
};
}
class PayloadUser {
final String name;
final bool active;
PayloadUser({
required this.name,
required this.active,
});
factory PayloadUser.fromJson(Map<String, dynamic> json) => PayloadUser(
name: json['name'] as String,
active: json['active'] as bool,
);
Map<String, dynamic> toJson() => {
'name': name,
'active': active,
};
}객체마다 final 필드, 이름 있는 매개변수 생성자, fromJson, toJson이 있는 클래스가 만들어집니다. 루트 클래스가 먼저 나오고 배열은 명시적인 타입 변환 map으로 디코딩됩니다.
{"first_name":"Ada","class":"A","2fa":true,"ratio":0.5}class Payload {
final String firstName;
final String classValue;
final bool field2fa;
final double ratio;
Payload({
required this.firstName,
required this.classValue,
required this.field2fa,
required this.ratio,
});
factory Payload.fromJson(Map<String, dynamic> json) => Payload(
firstName: json['first_name'] as String,
classValue: json['class'] as String,
field2fa: json['2fa'] as bool,
ratio: (json['ratio'] as num).toDouble(),
);
Map<String, dynamic> toJson() => {
'first_name': firstName,
'class': classValue,
'2fa': field2fa,
'ratio': ratio,
};
}필드는 camelCase이고 원래 키는 fromJson과 toJson의 따옴표 문자열 안에 남습니다. Dart 키워드에는 Value 접미사가, 숫자로 시작하는 이름에는 field 접두사가 붙고 double은 num을 거칩니다.
{"deleted_at":null,"tags":[],"scores":[1,null,3],"mixed":[1,"a"]}class Payload {
final dynamic deletedAt;
final List<dynamic> tags;
final List<int?> scores;
final List<dynamic> mixed;
Payload({
this.deletedAt,
required this.tags,
required this.scores,
required this.mixed,
});
factory Payload.fromJson(Map<String, dynamic> json) => Payload(
deletedAt: json['deleted_at'],
tags: json['tags'] as List<dynamic>,
scores: (json['scores'] as List<dynamic>).map((e0) => e0 as int?).toList(),
mixed: json['mixed'] as List<dynamic>,
);
Map<String, dynamic> toJson() => {
'deleted_at': deletedAt,
'tags': tags,
'scores': scores,
'mixed': mixed,
};
}null만 있는 키는 타입 정보가 없어 선택 dynamic이 됩니다. 빈 배열이나 섞인 배열은 List<dynamic>이고, 정수 사이에 null이 있는 배열은 List<int?>가 됩니다.
[{"id":1,"name":"Ada"},{"id":2,"email":"grace@example.com"}]typedef Payload = List<PayloadItem>;
class PayloadItem {
final int id;
final String? name;
final String? email;
PayloadItem({
required this.id,
this.name,
this.email,
});
factory PayloadItem.fromJson(Map<String, dynamic> json) => PayloadItem(
id: json['id'] as int,
name: json['name'] as String?,
email: json['email'] as String?,
);
Map<String, dynamic> toJson() => {
'id': id,
if (name != null) 'name': name,
if (email != null) 'email': email,
};
}두 원소가 PayloadItem 하나로 합쳐집니다. name과 email은 한쪽 원소에만 있어 널 허용이고 required가 아니며 null이면 toJson에서 빠집니다. typedef가 가장 앞에 나옵니다.
{"grid":[[1,2],[3]],"prices":[19,19.5]}class Payload {
final List<List<int>> grid;
final List<double> prices;
Payload({
required this.grid,
required this.prices,
});
factory Payload.fromJson(Map<String, dynamic> json) => Payload(
grid: (json['grid'] as List<dynamic>).map((e0) => (e0 as List<dynamic>).map((e1) => e1 as int).toList()).toList(),
prices: (json['prices'] as List<dynamic>).map((e0) => (e0 as num).toDouble()).toList(),
);
Map<String, dynamic> toJson() => {
'grid': grid,
'prices': prices,
};
}중첩 리스트는 List.from 한 번이 아니라 e0, e1 캐스트로 단계마다 디코딩되고, 19와 19.5가 함께 있으면 List<double>로 합쳐집니다.
자주 하는 실수
샘플에서 null이던 키는 타입을 알 수 없습니다
붙여넣은 모든 객체에서
deleted_at이null이면 문자열인지 알 수 없어 필드가dynamic이 됩니다. 값이 들어 있는 레코드를 함께 붙여넣거나,dynamic을 실제 널 허용 타입으로 직접 바꾸세요.샘플 하나로는 키가 항상 있다고 증명할 수 없습니다
붙여넣은 모든 원소에 있는 키는 널 비허용
required로 생성됩니다. API가 그 키를 생략할 수 있다면 디코딩 때 캐스트가 실패합니다. 레코드를 더 붙여넣으면 일부에서 빠진 키가 널 허용으로 바뀝니다.소수점·지수 표기는 double이 됩니다
19.0과1e3은 값이 정수여도double이 됩니다.19처럼 정수로 적은 값만int로 남으므로 API의 실제 숫자 표기를 샘플에 반영하세요.한 위치의 섞인 모양은 dynamic입니다
value가 어떤 원소에서는 문자열이고 다른 원소에서는 숫자라면 필드는dynamic이 되고 런타임 타입에 따른 분기는 직접 써야 합니다. 객체와 단일 값이 섞인 배열은List<dynamic>입니다.큰 정수는 이미 반올림되어 있습니다
타입 추론에서 9007199254740993을 JavaScript 숫자로 바꾸면 9007199254740992가 되므로 2^53을 넘는 정수는 자릿수를 잃을 수 있습니다. Dart VM의
int는 64비트지만 반올림된 자릿수를 되살리지는 못합니다. 정확한 ID는 문자열로 입력하고String을 쓰세요.
자주 묻는 질문
json_serializable이나 freezed를 쓰나요?
아니요. fromJson과 toJson이 명시된 일반 클래스를 만들어서 build_runner 단계가 없습니다.
JSON 배열을 붙여넣어도 되나요?
네. 원소들이 클래스 하나로 합쳐지고 typedef가 목록 이름을 정합니다. 예: typedef Payload = List<PayloadItem>;. 단일 값 배열이면 typedef만 나옵니다.
선택 키는 어떻게 처리되나요?
배열의 일부 객체에서 빠진 키는 생성자에서 required가 아닌 널 허용 필드가 됩니다. toJson은 값이 null이 아닐 때만 그 키를 씁니다.
null 값은 어떤 타입이 되나요?
어떤 샘플에서는 null이고 다른 샘플에서는 문자열이면 String?이 됩니다. 모든 샘플에서 null이면 타입을 알 수 없어 dynamic이 됩니다.
double이어야 할 필드가 왜 int로 나오나요?
샘플이 19처럼 정수 표기를 썼을 때만 int입니다. 19.0이나 1e3은 double이 되고 (json['price'] as num).toDouble()로 디코딩됩니다.
class 같은 Dart 키워드는 어떻게 되나요?
필드는 classValue가 되고 fromJson과 toJson의 문자열 리터럴은 그대로 class를 씁니다.
루트 클래스 이름을 바꿀 수 있나요?
네. "루트 타입 이름"을 바꾸면 됩니다. 기본값은 Payload이고 PayloadUser처럼 중첩 클래스 이름도 여기서 만들어집니다.
JSON이 어딘가로 전송되나요?
아니요. 변환은 브라우저에서만 이루어집니다.
마지막 업데이트:
