[TIL-023] Dart에서 json사용하기

jsonEncode

타입을 `String`으로 변환하는 함수.

`import 'dart:convert';`라이브러리를 통해 사용할 수 있다.

import 'dart:convert';

void main() {
  Map<String, dynamic> myInfo = {
    "name": "Mark", 
    "age": 20
  };

  // Map -> String
  String jsonString = jsonEncode(myInfo);
  print(jsonString); // {"name":"Mark","age":20}
  print(jsonString.runtimeType); // String

  // List -> String
  List list = [myInfo, myInfo];
  String listJsonString = jsonEncode(list);
  print(listJsonString); // [{"name":"Mark","age":20},{"name":"Mark","age":20}]
  print(listJsonString.runtimeType); // String
}

 

jsonDecode

Map형태의 `String`타입이나 List형태의 `String`타입을 각 형태의 맞는 타입으로 변환하는 함수.

`import 'dart:convert';` 라이브러리를 통해 사용할 수 있다.

import 'dart:convert';

void main() {
  // 여러줄의 문자열, 따옴표 등을 편하게 사용할 수 있는 트리플 쿼트 """, '''
  String jsonString = """
{
  "name": "Mark",
  "age": 20
}
""";

  // jsonDecode 리턴 타입이 dynamic 이니까 var를 사용해도 됨
  // Map형태의 String 타입 -> Map 타입
  var result = jsonDecode(jsonString);
  print(result); // {name: 'Mark', age: 20}
  print(result.runtimeType); // Map<String, dynamic>
}
import 'dart:convert';

void main() {
  String jsonString = """
[
  {
    "name": "Mark",
    "age": 20
  },
  {
    "name": "kai",
    "age": 7
  }
]
""";

  // List구조의 String 타입 -> List 타입
  var result = jsonDecode(jsonString);
  print(result); // [{name: Mark, age: 20}, {name: kai, age: 7}]
  print(result.runtimeType); // List<dynamic>
}

 

클래스 적용

jsonDecode의 값으로 네임드 생성자 만들기 - 생성자.fromJson

jsonDecode의 값으로 클래스에 적용할 수 있다.

적용하는 이유는 `Map`에서 값을 사용하기 위해서는 대괄호를 사용해야하고 어떤 키값이 있는지 자동완성이 되지 않지만 `클래스를 이용한 인스턴스는 점(.)을 이용할 때 자동완성이 되어 어떤 속성이 있는지 파악할 수 있어 좋다.`

`fromJson이라는 명칭은 개발자들 사이에서의 컨벤션이라 볼 수 있다.`

import 'dart:convert';

class User {
  String name;
  int age;

  User({
    required this.name, 
    required this.age
  });

  // this 생략 가능
  User.fromJson(Map<String, dynamic> map)
    : this.name = map['name'],
      this.age = map['age'];

  // 위 방법 말고 이렇게 사용할 수도 있음
  // 네임드 생성자 호출시 기본 생성자를 호출한다는 뜻
  User.fromJson2(Map<String, dynamic> map)
    : this(name: map['name'], age: map['age']);
}

void main() {
  String jsonString = """
{
  "name": "Mark",
  "age": 20
}
""";

  // 'Map형태의 String 타입 -> Map 타입' 변환
  var jsonMap = jsonDecode(jsonString);

  // 네임드 생성자인 fromJson을 만들어 'Map 타입 -> 인스턴스' 변환
  User user = User.fromJson(jsonMap);
  print(user.name); // Mark
  print(user.age); // 20
}

 

toJson 만들기

json형태로 만들기 위해서는 `Map`, `List`여야 한다.

그래서 `toJson`메서드를 만들어 타입을 변경하여 반환한다. 

`toJson이라는 명칭은 개발자들 사이에서의 컨벤션이라 볼 수 있다.`

import 'dart:convert';

void main() {
  String jsonString = """
{
  "name": "Mark",
  "age": 20
}
""";

  // 'Map형태의 String 타입 -> Map 타입' 변환
  var jsonMap = jsonDecode(jsonString);

  // 네임드 생성자인 fromJson을 만들어 'Map 타입 -> 인스턴스' 변환
  User user = User.fromJson(jsonMap);

  // toJson 메서드를 만들어 '인스턴스 -> Map 타입' 변환
  Map<String, dynamic> userMap = user.toJson();
  print(userMap['name']); // Mark
  print(userMap['age']); // 20
}

class User {
  String name;
  int age;

  User({required this.name, required this.age});

  // this 생략 가능
  User.fromJson(Map<String, dynamic> map)
    : this.name = map['name'],
      this.age = map['age'];

  // 위 방법 말고 이렇게 사용할 수도 있음
  // 네임드 생성자 호출시 기본 생성자를 호출한다는 뜻
  User.fromJson2(Map<String, dynamic> map)
    : this(name: map['name'], age: map['age']);

  // 클래스의 인스턴스를 다시 json으로 만들기 위해
  // Map타입으로 변경하는 메서드를 정의
  Map<String, dynamic> toJson() {
    return {"name": name, "age": age};
  }
}

 

중첩된 형태의 데이터일 경우

import 'dart:convert';

void main() {
  String jsonString = """
{
  "name": "Mark",
  "age": 20,
  "isMale" : true,
  "favorite_foods" : ["삼겹살", "팥빙수", "샤인머스켓"],
  "contact": {
    "mobile": "010-0000-0000",
    "email": null
  }
}
""";

  // 2. Map<String, dynamic>으로 변환해서 map이라는 이름의 변수에 담기
  Map<String, dynamic> map = jsonDecode(jsonString);

  // 3. jsonString 을 Map 으로 변환 후 User class로 변환 후 출력해서 변수에 담기!
  User user = User.fromJson(map);

  // 4. print 문으로 Pet 클래스의 toJson 메서드를 호출해서 출력
  print(user.toJson());
}

/*
 * 1. 아래에서 클래스 정의 Contact, User
 */

class User {
  final String name;
  final int age;
  final bool isMale;
  final List<dynamic> favoriteFoods;
  final Contact contact;

  User({
    required this.name,
    required this.age,
    required this.isMale,
    required this.favoriteFoods,
    required this.contact,
  });

  User.fromJson(Map<String, dynamic> json)
    : this(
        name: json["name"],
        age: json["age"],
        isMale: json["isMale"],
        
        // 5. List를 참조가 아닌 복사를 위해 List.from 사용
        favoriteFoods: List.from(json["favorite_foods"]),

        // 6. 새로운 인스턴스로 만들었으니 참조가 될 수 없음
        contact: Contact.fromJson(json["contact"]),
      );

  Map<String, dynamic> toJson() {
    return {
      "name": name,
      "age": age,
      "isMale": isMale,
      "favorite_foods": favoriteFoods,
      "contact": contact.toJson(),
    };
  }
}

class Contact {
  String mobile;
  String? email;

  Contact({required this.mobile, required this.email});

  Contact.fromJson(Map<String, dynamic> json)
    : this(mobile: json["mobile"], email: json["email"]);

  Map<String, dynamic> toJson() {
    return {"mobile": mobile, "email": email};
  }
}

 

1. `jsonDecode`로 인해 `Map`타입이 되었는데 값에 `Map`타입이 또 있으면 내부에 있는 값에 대한 클래스를 먼저 정의하고, 전체를 포함하는 클래스를 나중에 정의하는 것이 편하다.

5. `map['favorite_foods']`를 복사하기 위해 `List.from`을 사용. 제네릭 `List<String>.from`을 사용하여 `map['favorite_foods']`의 타입인 `List<dynamic>`타입을 `List<String>`타입으로 변경하여 복사할 수도 있다.

6. `Contact.fromJson`으로 새로운 인스턴스를 만들었으므로 참조가 될 수 없다. 

 

느낀 점

`fromJson`은 생성자로 사용하고, `toJson`은 메서드로 사용하는 이유가 뭘까 생각해봤다.

 

생성자는 `인스턴스`를 반환하기 위한 메서드이므로 `fromJson`을 네임드 생성자로 사용하는 것 같다.

반면에, `toJson`은 인스턴스가 아닌 `Map`을  반환하기 때문에 생성자라는 역할 보다는 메서드라는 역할이 더 잘 어울리는 것 같다.