【入門編】Null安全環境での『JSONパース』:freezed vs json_serializableの型安全性比較 – Dart コア文法・オブジェクト指向・Null安全解析バイブル

Null安全環境でのJSONパース:freezed vs json_serializableの型安全性比較

皆さん、こんにちは!Dartの世界へようこそ。今日は、外部APIから受け取ったJSONデータを、Null安全なDartコードで安全に扱うための、とっても重要なトピックについてお話ししますね。特に、`freezed`と`json_serializable`という、JSONパースでよく使われるライブラリを、Null安全の観点からじっくり比較していきましょう。

「APIから返ってくるデータって、たまにnullだったり、型が違ったりして、アプリがクラッシュしちゃうことがあるんだよな…」そんな経験、ありませんか?DartのNull安全機能は、こうした問題を未然に防ぎ、コードの安全性をグッと高めてくれます。このNull安全環境で、どのようにJSONデータを扱うのがベストなのか、一緒に学んでいきましょう!

なぜJSONパースでNull安全が重要なのか?

まず、なぜJSONパースでNull安全がそんなに重要なのか、その理由から確認しておきましょう。

APIから受け取るJSONデータは、開発者が完全にコントロールできるものではありません。APIの仕様変更、ネットワークエラー、あるいは意図しないデータ形式など、様々な要因で予期せぬ値が返ってくる可能性があります。

例えば、以下のようなJSONレスポンスがあったとします。

{
“user_id”: 123,
“username”: “dart_dev”,
“email”: null, // メールアドレスはnullの場合がある
“profile_image_url”: “https://example.com/image.jpg”,
“age”: 30
}

この`email`フィールドが`null`だった場合、Null安全が有効でないDartコードだと、`user.email.toLowerCase()`のようにアクセスしようとした瞬間に、`NullPointerException`(あるいはそれに類するエラー)が発生し、アプリがクラッシュしてしまう可能性があります。

DartのNull安全は、コンパイル時にこのような「nullによるエラー」を検出・防止してくれる、まさに「守護神」のような存在です。JSONパースにおいても、このNull安全の恩恵を最大限に受けることが、堅牢なアプリケーション開発の鍵となります。

JSONパースライブラリの役割

JSONパースライブラリは、このJSON文字列をDartのオブジェクト(クラスのインスタンス)に変換する作業を、効率的かつ安全に行うためのツールです。

例えば、上記のJSONをDartの`User`オブジェクトとして扱いたい場合、手書きでパース処理を書くのは大変ですよね。ライブラリを使えば、JSONのキーとDartのフィールドをマッピングし、自動的に変換してくれるんです。

そして、Null安全環境では、この「マッピング」の際に、JSONから取得した値が`null`だった場合にどう扱うかを、より厳密に定義できるようになります。

`freezed` vs `json_serializable`:Null安全に注目した比較

さて、本題です。JSONパースでよく使われる代表的なライブラリとして、`freezed`と`json_serializable`があります。どちらも非常に強力で人気がありますが、Null安全との関わり方には少し違いがあります。

1. `json_serializable`:シンプルで強力なJSONマッパー

`json_serializable`は、Dartのコード生成ライブラリの一つで、JSONとDartオブジェクト間のシリアライズ/デシリアライズ(変換)を自動生成してくれます。アノテーションを使って、JSONのキーとDartのフィールドを紐づけるのが特徴です。

基本的な使い方

まず、`pubspec.yaml`に依存関係を追加します。

pubspec.yaml
dependencies:
flutter:
sdk: flutter
json_annotation: ^4.8.1 # JSONキーとDartフィールドのマッピング用
json_serializable: ^6.7.1 # JSON変換コード生成用

dev_dependencies:
build_runner: ^2.4.6 # コード生成を実行するために必要

そして、Dartコードでモデルクラスを定義します。

// lib/models/user.dart
import ‘package:json_annotation/json_annotation.dart’;

// JSONのキーとDartのフィールド名が異なる場合に、@JsonKeyでマッピングできます。
// この例では、JSONの ‘user_id’ が Dartの ‘id’ に対応します。
@JsonSerializable()
class User {
final int id;
final String username;
final String? email; // Null許容型として定義
final String? profileImageUrl; // Null許容型として定義
final int? age; // Null許容型として定義

User({
required this.id,
required this.username,
this.email,
this.profileImageUrl,
this.age,
});

// generated code
factory User.fromJson(Map json) => _$UserFromJson(json);
Map toJson() => _$UserToJson(this);
}

ポイント:Null許容型の定義

  • `email`、`profileImageUrl`、`age`は、JSONで`null`が許容されるため、Dartでも`String?`や`int?`のように、Null許容型として定義します。
  • `id`や`username`のように、必ず存在すべきフィールドは`required`キーワードを付けて、Null許容型ではない(Non-nullable)型で定義します。JSONにこのフィールドが存在しない場合、`json_serializable`はエラーを発生させてくれます(デフォルト設定の場合)。

コード生成を実行します。ターミナルでプロジェクトのルートディレクトリに移動し、以下のコマンドを実行してください。

flutter pub run build_runner build

これにより、`lib/models/user.g.dart`というファイルが自動生成されます。このファイルに、JSONを`User`オブジェクトに変換する`_$UserFromJson`関数などが含まれています。

JSONパースの実行例

// lib/main.dart
import ‘package:flutter/material.dart’;
import ‘models/user.dart’; // 先ほど作成したUserモデルをインポート

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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text(‘JSON Parsing Example’)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: () {
// 例1: 正常なJSONデータ
final jsonString1 = ”’
{
“user_id”: 123,
“username”: “dart_dev”,
“email”: “dart@example.com”,
“profile_image_url”: “https://example.com/image.jpg”,
“age”: 30
}
”’;
final user1 = User.fromJson(Map.from(jsonDecode(jsonString1)));
print(‘User 1: ${user1.username}, Email: ${user1.email}’); // 出力: User 1: dart_dev, Email: dart@example.com

// 例2: emailがnullのJSONデータ
final jsonString2 = ”’
{
“user_id”: 456,
“username”: “anonymous”,
“email”: null,
“profile_image_url”: null,
“age”: null
}
”’;
final user2 = User.fromJson(Map.from(jsonDecode(jsonString2)));
print(‘User 2: ${user2.username}, Email: ${user2.email ?? ‘N/A’}’); // 出力: User 2: anonymous, Email: N/A

// 例3: 必須フィールド(username)が欠損している場合(エラーになる!)
// final jsonString3 = ”’
// {
// “user_id”: 789,
// “email”: “error@example.com”
// }
// ”’;
// try {
// final user3 = User.fromJson(Map.from(jsonDecode(jsonString3)));
// print(‘User 3: ${user3.username}’);
// } catch (e) {
// print(‘Error parsing JSON 3: $e’); // エラーが出力される
// }
},
child: const Text(‘Parse JSON’),
),
],
),
),
),
);
}
}

// jsonDecodeを使うために必要
import ‘dart:convert’;

実行結果例(コンソール出力):

User 1: dart_dev, Email: dart@example.com
User 2: anonymous, Email: N/A

JSONパース時のエラーについて:
`json_serializable`は、デフォルトでは`required`とマークされたフィールドがJSONに存在しない場合、コンパイル時ではなく実行時にエラーを投げます。Null安全の恩恵は、Null許容型を正しく定義することで、`null`値へのアクセスによるクラッシュを防ぐ点にあります。

2. `freezed`:イミュータブルなデータクラスと強力なコード生成

`freezed`は、Dartのデータクラスをより安全に、そして便利に定義するためのライブラリです。`json_serializable`の機能も内包しており、さらにイミュータビリティ(不変性)や、`union types`(sealed classesのようなもの)といった強力な機能を提供します。

基本的な使い方

まず、`pubspec.yaml`に依存関係を追加します。

pubspec.yaml
dependencies:
flutter:
sdk: flutter
freezed_annotation: ^2.4.1 # freezedのアノテーション用
json_annotation: ^4.8.1 # json_serializableとの連携用
freezed: ^2.4.5 # freezed本体

dev_dependencies:
build_runner: ^2.4.6
freezed_annotation: ^2.4.1 # freezedのアノテーション用
json_annotation: ^4.8.1 # json_serializableとの連携用

そして、`freezed`を使ってモデルクラスを定義します。`abstract class`と`part`ディレクティブを使うのが特徴です。

// lib/models/user.freezed.dart (自動生成されるファイル)
// lib/models/user.g.dart (json_serializableによって生成されるファイル)
//
// このファイルは手書きせず、以下のUserクラス定義から生成されます。

// lib/models/user.dart
import ‘package:freezed_annotation/freezed_annotation.dart’;

part ‘user.freezed.dart’; // freezedによるコード生成用
part ‘user.g.dart’; // json_serializableによるコード生成用

// @freezedアノテーションを付け、abstract classを定義します。
@freezed
class User with _$User {
// nullableなフィールドはNull許容型 (? を付ける) で定義します。
// requiredなフィールドは、デフォルトでNon-nullableとして扱われます。
// JSONのキーとDartのフィールド名が異なる場合は、@JsonKeyでマッピングします。
const factory User({
required int id,
required String username,
String? email, // Null許容型
@JsonKey(name: ‘profile_image_url’) String? profileImageUrl, // Null許容型、キー名マッピング
int? age, // Null許容型
}) = _User;

// JSONからオブジェクトを生成するためのファクトリコンストラクタ
// json_serializableがこれを生成します。
factory User.fromJson(Map json) => _$UserFromJson(json);
}

コード生成を実行します。

flutter pub run build_runner build

`freezed`は、`_User`という実装クラスを`_User.freezed.dart`に、JSON変換コードを`_User.g.dart`に生成します。

JSONパースの実行例

パースの実行方法は、`json_serializable`を使った場合とほとんど同じです。

// lib/main.dart (user1, user2 のパース部分は上記と同じ)
import ‘package:flutter/material.dart’;
import ‘models/user.dart’; // freezedで定義したUserモデルをインポート
import ‘dart:convert’; // jsonDecodeのために必要

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

class MyApp extends StatelessWidget {
const MyApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text(‘JSON Parsing Example’)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: () {
// 例1: 正常なJSONデータ
final jsonString1 = ”’
{
“user_id”: 123,
“username”: “dart_dev”,
“email”: “dart@example.com”,
“profile_image_url”: “https://example.com/image.jpg”,
“age”: 30
}
”’;
// freezedはfromJsonメソッドを自動生成してくれます
final user1 = User.fromJson(jsonDecode(jsonString1) as Map);
print(‘User 1: ${user1.username}, Email: ${user1.email}’); // 出力: User 1: dart_dev, Email: dart@example.com

// 例2: emailがnullのJSONデータ
final jsonString2 = ”’
{
“user_id”: 456,
“username”: “anonymous”,
“email”: null,
“profile_image_url”: null,
“age”: null
}
”’;
final user2 = User.fromJson(jsonDecode(jsonString2) as Map);
print(‘User 2: ${user2.username}, Email: ${user2.email ?? ‘N/A’}’); // 出力: User 2: anonymous, Email: N/A

// 例3: 必須フィールド(username)が欠損している場合(エラーになる!)
// final jsonString3 = ”’
// {
// “user_id”: 789,
// “email”: “error@example.com”
// }
// ”’;
// try {
// final user3 = User.fromJson(jsonDecode(jsonString3) as Map);
// print(‘User 3: ${user3.username}’);
// } catch (e) {
// print(‘Error parsing JSON 3: $e’); // エラーが出力される
// }
},
child: const Text(‘Parse JSON’),
),
],
),
),
),
);
}
}

実行結果例(コンソール出力):

User 1: dart_dev, Email: dart@example.com
User 2: anonymous, Email: N/A

`freezed`のNull安全への貢献:
`freezed`は、`json_serializable`の機能に加えて、クラス自体の定義をより安全にします。

  • イミュータビリティ: `freezed`で生成されるクラスはデフォルトでイミュータブル(不変)です。一度作成されたオブジェクトは変更できません。これは、状態管理などで意図しないデータの変更を防ぐのに役立ち、Null安全と相まってコードの予測可能性を高めます。
  • コンパイル時のエラーチェック: `freezed`は、`required`フィールドが欠損している場合、`json_serializable`と同様に実行時にエラーを投げますが、`freezed`の生成コードはより洗練されており、将来的なNull安全の進化にも対応しやすい設計になっています。

どちらを選ぶべきか?:型安全性と開発効率のバランス

では、`freezed`と`json_serializable`、どちらを選ぶのが良いのでしょうか?

  • `json_serializable`:
  • メリット: 導入が比較的容易で、JSONマッピングに特化しているため、シンプルにJSONパースだけを行いたい場合に最適です。既存のプロジェクトへの導入もしやすいでしょう。
  • デメリット: クラス自体のイミュータビリティや、`union types`のような高度な機能は提供されません。
  • `freezed`:
  • メリット: イミュータブルなデータクラスを生成し、`union types`(`when`や`maybeWhen`メソッドでパターンマッチング可能)といった強力な機能を提供します。これにより、より安全で保守性の高いコードを書くことができます。JSONパース機能も内包しており、`json_serializable`と組み合わせることで、JSONパースとデータクラス定義の両方を`freezed`で管理できます。
  • デメリット: `json_serializable`単体よりも学習コストが若干高くなる可能性があります。コード生成も複数(`.freezed.dart`と`.g.dart`)行われます。

型安全性の比較:
Null安全という観点では、どちらのライブラリも、Null許容型 (`?` を付けた型) を正しく定義していれば、`null`値による実行時エラーを防ぐことができます。

  • `json_serializable`では、`String?`のように明示的にNull許容型として定義する必要があります。
  • `freezed`でも同様に、`String?`のように定義します。

どちらのライブラリも、DartのNull安全の仕組みを最大限に活用できるように設計されています。

結論として:

  • JSONパースのみが目的で、クラスのイミュータビリティなどにこだわりがない場合: `json_serializable`はシンプルで良い選択肢です。
  • イミュータブルなデータクラスを定義したい、`union types`のような強力な機能を使いたい、将来的な拡張性も考慮したい場合: `freezed`は非常に強力な選択肢となります。`freezed`を使う場合でも、JSONパースのために`json_serializable`の機能も併せて利用することになります(`freezed`が内部で`json_serializable`のコード生成を呼び出す形です)。

多くのモダンなDart/Flutterプロジェクトでは、イミュータビリティや強力なデータクラス定義ができる`freezed`が好まれる傾向にあります。Null安全との親和性も高く、開発体験を向上させてくれるからです。

陥りやすい文法エラーとベストプラクティス

JSONパース、特にNull安全環境で開発する際に、よくある落とし穴と、その回避策を見ていきましょう。

1. Null許容型 (`?`) の付け忘れ

エラー例:

// Userクラス定義で email を String? にすべきところを String にしてしまった
@JsonSerializable()
class User {
final int id;
final String username;
final String email; // <-- ここを String? にすべきだった! User({required this.id, required this.username, required this.email}); factory User.fromJson(Map json) => _$UserFromJson(json);
Map toJson() => _$UserToJson(this);
}

// … JSONパース部分 …
final jsonString = ”’
{
“user_id”: 123,
“username”: “dart_dev”,
“email”: null // JSONではnullなのに、DartではString型として定義してしまった
}
”’;
// この行でエラーが発生する!
// final user = User.fromJson(Map.from(jsonDecode(jsonString)));

解説:
JSONの`email`フィールドは`null`になりうるのに、Dartの`User`クラスで`email`を`String`(Null許容ではない型)として定義してしまうと、`json_serializable`(または`freezed`)がJSONの`null`値を`String`型に変換しようとしてエラーになります。

ベストプラクティス:
APIドキュメントをよく確認し、`null`になりうるフィールドは必ずDartでNull許容型 (`?` を付けた型、例: `String?`, `int?`) として定義しましょう。

// 正しい定義
@JsonSerializable()
class User {
final int id;
final String username;
final String? email; // Null許容型に修正!

User({required this.id, required this.username, this.email}); // requiredを外すか、null許容型ならデフォルト値渡しでもOK

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

2. `required` キーワードとJSONの欠損フィールド

エラー例:

// Userクラス定義で username を required にしたが、JSONに存在しない
@JsonSerializable()
class User {
final int id;
final String? username; // <-- requiredではなく、null許容型にしておくべきだった final String? email; User({required this.id, this.username, this.email}); factory User.fromJson(Map json) => _$UserFromJson(json);
Map toJson() => _$UserToJson(this);
}

// … JSONパース部分 …
final jsonString = ”’
{
“user_id”: 123,
“email”: “dart@example.com”
// “username” が欠損している!
}
”’;
// この行でエラーが発生する!
// final user = User.fromJson(Map.from(jsonDecode(jsonString)));

解説:
`required`キーワードは、コンストラクタにその引数が必須であることを示します。`json_serializable`や`freezed`は、この`required`フィールドがJSONに存在しない場合、デフォルトでは実行時にエラーを発生させます。APIからのレスポンスが必ずしも完璧ではない場合、`required`フィールドは慎重に使う必要があります。

ベストプラクティス:

  • APIからのレスポンスで、絶対に欠損してはならないフィールドにのみ`required`を付けましょう。
  • `null`になりうる、あるいはAPIによっては欠損する可能性のあるフィールドは、Null許容型 (`?`) で定義し、`required`を付けないようにしましょう。

// ベストプラクティス: usernameはNull許容型に
@JsonSerializable()
class User {
final int id;
final String? username; // Null許容型にする
final String? email;

User({required this.id, this.username, this.email}); // requiredはidのみ

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

3. `json_decode` のキャスト忘れ

JSON文字列をパースする際、`dart:convert`の`jsonDecode`関数は`dynamic`型の値を返します。これを`Map`として扱うためには、適切にキャストする必要があります。

エラー例:

import ‘dart:convert’;

// … Userクラス定義 …

// jsonDecodeの結果をキャストしない場合
final jsonString = ”’
{
“user_id”: 123,
“username”: “dart_dev”,
“email”: null
}
”’;
// final dynamic decodedJson = jsonDecode(jsonString);
// final user = User.fromJson(decodedJson); // <-- ここで型エラー! 解説:
`User.fromJson`は`Map`を引数に取りますが、`jsonDecode`の返り値は`dynamic`です。Dartは型安全な言語なので、`dynamic`を直接`Map`として渡すことはできません。

ベストプラクティス:
`jsonDecode`の結果は、`as Map`などを使って明示的にキャストしましょう。

final dynamic decodedJson = jsonDecode(jsonString);
final user = User.fromJson(decodedJson as Map);

あるいは、`jsonDecode`の直後にキャストすることもできます。

final user = User.fromJson(jsonDecode(jsonString) as Map);

まとめ:Null安全で安全なJSONパースの未来

今日は、DartのNull安全環境におけるJSONパースの重要性と、`freezed`および`json_serializable`という二つの強力なライブラリについて、型安全性の観点から比較してきました。

  • Null安全は、`null`による実行時エラーを防ぎ、コードの堅牢性を格段に向上させます。
  • JSONパースにおいては、APIからのレスポンスを想定し、Null許容型 (`?`) を適切に定義することが最も重要です。
  • `json_serializable`はJSONマッピングに特化したシンプルで強力なライブラリです。
  • `freezed`は、イミュータブルなデータクラス定義や`union types`といった高度な機能を提供し、より安全で保守性の高いコードを書くのに役立ちます。

どちらのライブラリを選んだとしても、DartのNull安全の原則を理解し、モデルクラスの型定義を丁寧に行うことで、APIからのデータ取得に関する多くの問題を未然に防ぐことができます。

「ここをクリアすれば、DartのJSONパースはバッチリマスターできますよ!」という言葉を胸に、ぜひ皆さんのプロジェクトでNull安全なJSONパースを実践してみてください。きっと、より安定した、より信頼性の高いアプリケーション開発ができるはずです。

もし、さらに深く知りたいことや、疑問点があれば、いつでも聞いてくださいね!

タイトルとURLをコピーしました