Flutter

3. JasonSerializable(JSON의 직렬화)

모리선생 2023. 3. 14. 23:03
728x90

목표

JsonSerializable을 사용하여 모델을 생성할 시에 JSON을 객체로 Serialize하는 코드를 작성할 필요가 없도록 하는 인터페이스에 대해서 알아보자. JsonSerializable의 기본 사용법을 확인해보고 Pagination과 연동하여 Inheritance의 적용법도 공부해보자.


* 해당 내용은 본인의 개발 공부에 있어서 여러가지 참고하며 기록했던 내용을 복기하고 이해하고자 작성한 블로그 글입니다. 같이 공부를 해나가며 Flutter라는 언어에 취미를 가지신 분 들이 개념을 잡는데 도움이 되었으면 합니다. 참고한 홈페이지 등에 대해서는 각 페이지별 하단에 명시하여 두었습니다. 수정 및 문의 사항이 있으면 알려주시길 바랍니다.

정의

JsonSerializable은 모델을 생성할시 JSON을 객체로 Serialize하는 코드를 작성할 필요가 없도록 한다.

 

추가 설명

JsonSerializable은 우편물을 보내는 것과 비유할 수 있다. 

1. 우편물을 상자에 넣고

2. 상자에 받는 사람의 이름과 주소를 적고

3. 상자를 우체국에 맡기면

4. 받는 사람이 있는 곳에 배달 된다

 

이를 다시 JsonSerializable에 맞게 설명하면

1. 우편물을 상자에 넣고 = 클래스의 인스턴스를 상자에 넣는다 (JSON 인코딩)

2. 상자에 받는 사람의 이름과 주소를 적고 = 상자에 키와 값의 쌍으로 된 라벨을 붙인다 (JSON 형식)

3. 상자를 우체국에 맡기면 = 인터넷을 통해 다른 컴퓨터나 앱으로 보내면 (JSON 전송)

4. 받는 사람이 있는 곳에 배달 된다 = 받는 사람이 상자를 열고 물건을 꺼낸다 (JSON 디코딩)

 

우체국 직원 처럼 상자의 크기와 모양에 따라 적절한 라벨을 만들고, 받는 사람이 원하는 방식으로 배달한다. 클래스의 구조와 속성에 맞게 JSON 문자열을 만들고, 다른 언어나 플랫폼에서 원하는 방식으로 변환하여 준다.

 

용어

인코딩 (혹은 직렬화) - 자료 구조를 문자열로 변환하는 것

디코딩 (역직렬화) - 문자열을 자료구조로 변환하는 것

 

JSON 직렬화 종류

일반 직렬화 - 소규모 프로젝트에 적합

코드 생성을 이용한 자동화성 직렬화 - 중대형 프로젝트에 적합

특히 이러한 경우 JSON을 상자에 넣고 라벨을 붙이는 일을 다른 사람에게 맡기는 것과 같으며, 상자와 라벨을 만들어주는 방법을 알려주고 우리가 가지고 있는 물건의 종류와 모양에 따라 상자와 라벨을 만들어주는 도구를 알려주는 것과 같은 것이다.

 

코드 생성 라이브러리를 통한 JSON 직렬화(json_serializable 라이브러리)

참고: 영원한 패밀리 (Flutter(Dart) - Model 객체 Json 매핑 쉽게 하기(@JsonSerializable))

런타임시 JSON 직렬화 오류의 위험을 최소화 할 수 있음.

 

 

프로젝트에서 json_serializable 설정

프로젝트에서 json_serializable을 포함하기 위해서는 일반 dependency 한개와 개발 denpendency 두개가 필요합니다.

 

pubspec.yaml

dependencies:
  # 다른 의존성들
  json_annotation: ^2.0.0

dev_dependencies:
  # 다른 개발 의존성들
  build_runner: ^1.0.0
  json_serializable: ^2.0.0

 

json_serializable로 모델 클래스 생성

인코딩을 위한 코드와 JSON으로부터 name과 email 필드 디코딩 코드 생성한다.

해당 코드를 실행 후에는 ‘user.g.dart’;에 대해서는 “Target of URI hasn’t been generated: ‘user.g.dart’. 라는 오류 메시지가 팝업될 수 있다. 이 오류 메시지는 상자와 라벨을 만들어 주는 방법을 적어 놓은 파일이 없다는 것이다. 이는 직렬화 보일러 플레이트를 생성하여 해결이 가능하며, 해결방법은 다음과 같다.

> 일회성 코드 생성

> 지속적인 코드 생성

 

import 'package:json_annotation/json_annotation.dart';

// User라는 이름의 클래스 생성시 같은 파일에 있는 다른 코드들이 User 클래스의 비밀정보에 접근 할 수 있게 해준다.
// 비밀 정보랑 앞에 언더바가 붙은 변수나 함수를 말한다
// 이 구문을 쓰면 User 클래스와 관련된 자동 생성된 파일도 비밀정보에 접근할 수 있다.
// 자동생성된 파일은 원래 파일 이름뒤에 g.dart를 붙인 형식으로 만들어진다.

part 'user.g.dart';

/// 코드 생성기에 이 클래스가 JSON 직렬화 로직이 만들어져야 한다고 알려주는 어노테이션
@JsonSerializable()

class User {
  User(this.name, this.email);

  String name;
  String email;

// 팩토리 생성자는 새로운 인스턴스를 반환하는 특별한 생성자입니다. 
// _$UserFromJson() 생성자는 JSON 데이터를 매개변수로 받아서 User 클래스의 인스턴스를 만들어줍니다.
// 이 생성자의 이름은 User 클래스와 동일하게 시작하고 FromJson이라는 접미사가 붙습니다.

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);

 // `toJson`은 클래스가 JSON 인코딩 지원을 선언하는 규칙입니다.
 // 이의 구현은 생성된 private 헬퍼 메서드 `_$UserToJson`를 호출합니다.
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

 

json_serializable 모델 사용

디코딩

Map userMap = jsonDecode(jsonString);
var user = User.fromJson(userMap);

인코딩

String json = jsonEncode(user);

 

Nested Classes가 있을때의 코드

 

Class가 Nested가 된 경우에는 다음의 방식을 따른다

 

Address 클래스

import 'package:json_annotation/json_annotation.dart';
part 'address.g.dart';

@JsonSerializable()
class Address {
  String street;
  String city;
 
  Address(this.street, this.city);
 
  factory Address.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);
  Map<String, dynamic> toJson() => _$AddressToJson(this);
}

Address클래스가 User class 내에 nested된 상황

import 'address.dart';
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';

@JsonSerializable()
class User {
  String firstName; 
  Address address;
 
  User(this.firstName, this.address);

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

이러한 경우 인코딩과 디코딩을 진행하면 다음과 같이 진행할 수 있다

(
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{     
  'firstName': instance.firstName,
  'address': instance.address,     
};
Address address = Address("My st.", "New York");
User user = User("John", address);
print(user.toJson());

이렇게 하면 결과는 {name: John, address: Instance of 'address'}와 같은 결과를 얻을 것이다. 이 문제를 해결하기 위해서는 explictToJson: true를 첨부함으로써 원하는 결과를 얻을 수 있다. 수정된 예시는 다음과 같다.

import 'address.dart';
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';

@JsonSerializable(explicitToJson: true)
class User {
  String firstName; 
  Address address;
 
  User(this.firstName, this.address);

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

예제: Model 객체 Json 매핑하기 (JsonSerializable)

@pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  http: ^0.13.4 
  json_annotation: ^4.4.0

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^2.0.5
  json_serializable: ^6.1.5
  flutter_lints: ^1.0.0

 

lib/model.post.dart 생성

import 'package:json_annotation/json_annotation.dart';
part 'post.g.dart';

@JsonSerializable()
class Post {
final int userId;
final int id;
final String title;
final String body;

Post(this.userId, this.id, this.title, this.body);

factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);

Map<String, dynamic> toJson() => _$PostToJson(this);

@override
String toString() {
  return "Post userID [${userId}] id [${id}] title: $title";
}
}

참고: post.g.dart가 생성되지 않았으며, PostFromJson, PostToJson 모두 생성되지 않아서 생긴 밑줄이므로 무시하고 build runner를 시행한다

 

Terminal에서 build_runner실행

flutter pub run build_runner build

  • 저장된 클래스의 json매핑함수 (PostFromJson, PostToJson)을 실행
  • part ‘post.g.dart’로 지정한 파일이 모델 디렉토리에 생성

결과

[INFO] Generating build script...

[INFO] Generating build script completed, took 139ms

 

[INFO] Precompiling build script......

[INFO] Precompiling build script... completed, took 2.5s

 

[INFO] Initializing inputs

[INFO] Building new asset graph...

[INFO] Building new asset graph completed, took 318ms

 

[INFO] Checking for unexpected pre-existing outputs....

[INFO] Checking for unexpected pre-existing outputs. completed, took 0ms

 

[INFO] Running build...

[INFO] Generating SDK summary...

[INFO] 1.1s elapsed, 0/3 actions completed.

[INFO] Generating SDK summary completed, took 1.9s

 

[INFO] 2.9s elapsed, 0/3 actions completed.

[WARNING] json_serializable on lib/model/post.dart:

The version constraint "^4.4.0" on json_annotation allows versions before 4.8.0 which is not allowed.

[INFO] 6.4s elapsed, 0/3 actions completed.

[INFO] Running build completed, took 6.7s

 

[INFO] Caching finalized dependency graph...

[INFO] Caching finalized dependency graph completed, took 14ms

 

[INFO] Succeeded after 6.7s with 2 outputs (7 actions)

post.g.dart파일이 생성이 되었으며, 내용은 다음과 같음

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'post.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

Post _$PostFromJson(Map<String, dynamic> json) => Post(
    json['userId'] as int,
    json['id'] as int,
    json['title'] as String,
    json['body'] as String,
  );

Map<String, dynamic> _$PostToJson(Post instance) => <String, dynamic>{
    'userId': instance.userId,
    'id': instance.id,
    'title': instance.title,
    'body': instance.body,
  };

 

실제 모델 추가

post.dart 내에 다음의 코드 추가

void main() {
List<dynamic> jsonPosts = [
  {
    "userId": 1,
    "id": 1,
    "title":
    "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
    "body":
    "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto"
  },
  {
    "userId": 1,
    "id": 2,
    "title": "qui est esse",
    "body":
    "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla"
  }
];

}

 

main() 함수 내에 다음의 Json to Model 변환용 코드 추가

// Convert Json to Post model
  List<Post> posts = jsonPosts.map((e) => Post.fromJson(e)).toList();

  print(posts);

그 후 dart run lib/model/post.dart를 실행하면 다음과 같은 결과가 출력 됨

[Post userID [1] id [1] title: sunt aut facere repellat provident occaecati excepturi optio reprehenderit, Post userID [1] id [2] title: qui est esse]

 

main() 함수 내에 다음의 Model to Json 변환용 코드 추가

// Post model to Json 변환
Post newPost = Post(1, posts.length + 1, 'New title', 'Hello, all');
var json = newPost.toJson();
print(json.toString());

그 후 dart run lib/model/post.dart를 실행하면 다음과 같은 결과가 출력 됨

{userId: 1, id: 3, title: New title, body: Hello, all}


[사용예제] Openweather 측에서 받은 JSON 데이터를 JSONSerializable를 통해 정보를 받고 기상정보를 표현하는 UI 구성하기

 

coord.dart

import 'package:json_annotation/json_annotation.dart';

part 'coord.g.dart';

@JsonSerializable()
class Coord {
final double lon;
final double lat;

Coord({required this.lon, required this.lat});

factory Coord.fromJson(Map<String, dynamic> json) => _$CoordFromJson(json);
Map<String, dynamic> toJson() => _$CoordToJson(this);
}

detail.dart
import 'package:json_annotation/json_annotation.dart';

part 'detail.g.dart';

@JsonSerializable()
class Main {
final double temp;
@JsonKey(name: 'feels_like')
final double feelsLike;
@JsonKey(name: 'temp_min')
final double tempMin;
@JsonKey(name: 'temp_max')
final double tempMax;
final int pressure;
final int humidity;

Main({
  required this.temp,
  required this.feelsLike,
  required this.tempMin,
  required this.tempMax,
  required this.pressure,
  required this.humidity,
});

factory Main.fromJson(Map<String, dynamic> json) => _$MainFromJson(json);
Map<String, dynamic> toJson() => _$MainToJson(this);
}

 

open_weather.dart

import 'package:json_annotation/json_annotation.dart';

import 'coord.dart';
import 'detail.dart';
import 'weather.dart';

part 'open_weather.g.dart';

@JsonSerializable(explicitToJson: true)
class OpenWeather {
final Coord coord;
final List<Weather> weather;
final Main main;
final int visibility;

OpenWeather({
  required this.coord,
  required this.weather,
  required this.main,
  required this.visibility,
});

factory OpenWeather.fromJson(Map<String, dynamic> json) =>
    _$OpenWeatherFromJson(json);
Map<String, dynamic> toJson() => _$OpenWeatherToJson(this);
}

 

weather.dart

import 'package:json_annotation/json_annotation.dart';

part 'weather.g.dart';

@JsonSerializable()
class Weather {
final int id;
final String main;
final String description;
final String icon;

Weather({required this.id, required this.main, required this.description, required this.icon});

factory Weather.fromJson(Map<String, dynamic> json) =>
    _$WeatherFromJson(json);
Map<String, dynamic> toJson() => _$WeatherToJson(this);
}

 

main.dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:jsonserializable_listview/models/open_weather/open_weather.dart';
import 'package:http/http.dart' as http;

void main() {runApp(MyApp());
}

class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
  return MaterialApp(
    home: MyPage(),
  );
}
}

Future<OpenWeather> getWeather() async {
try {
  final url =
      Uri.parse('https://api.openweathermap.org/data/2.5/weather?q=seoul&appid=00f177de84b77619032754f5f2aa958b');

  final response = await http.get(url);
  final responseData = json.decode(response.body);
  final OpenWeather ow = OpenWeather.fromJson(responseData);

  print(ow.toJson());

  return ow;
} catch (err) {
  print(err);
  throw err;
}
}

class MyPage extends StatefulWidget {
const MyPage({Key? key}) : super(key: key);

@override
State<MyPage> createState() => _MyPageState();
}

class _MyPageState extends State<MyPage> {
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text('Open Weather'),
    ),
    body: FutureBuilder(
      future: getWeather(),
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          final ow = snapshot.data;

          return ListView(
            padding: EdgeInsets.fromLTRB(20, 20, 0, 0),
            children: <Widget>[
              Text(
                'longitude: ${ow?.coord.lon}',
                style: TextStyle(fontSize: 22),
              ),
              Text(
                'latitude: ${ow?.coord.lat}',
                style: TextStyle(fontSize: 22),
              ),
              Text(
                'weather id: ${ow?.weather[0].id}',
                style: TextStyle(fontSize: 22),
              ),
            ],
          );
        } else {
          return Center(
            child: CircularProgressIndicator(),
          );
        }
      },
    ),
  );
}
}

Reference

  1. https://youngwonhan-family.tistory.com/entry/FlutterDart-Model-객체-JsonSerializable-사용하여-쉽게-핸들링-하기
  2. 모델 https://jsonplaceholder.typicode.com
  3. https://flutter-ko.dev/docs/development/data-and-backend/json
  4. https://velog.io/@giyeon/flutter-3-json-handling-jsonserializable

 

728x90