🍆

What is json_serializable?

2024/08/08に公開

freezed使う時に入れるパッケージ

普段は直接使ってる感じではないので、ゆっくり勉強できるから、調べてみることにした。

json_serializable
build_runner

JSON を処理するための Dart Build System ビルダーを提供します。

ビルダーは、package:json_annotation で定義されたクラスでアノテーションされたメンバーを見つけるとコードを生成します。

  • クラスの JSON コードを生成するには、JsonSerializable をアノテートします。JsonSerializable に引数を指定して、生成されるコードを構成できます。また、JsonKey でアノテーションし、カスタム引数を指定することで、個々のフィールドをカスタマイズすることもできます。アノテーション値の詳細については、以下の表を参照してください。

  • JSON を含むファイルの内容で Dart フィールドを生成するには、JsonLiteral アノテーションを使用します。

Setup

json_serializableの最新リリース・バージョン用にプロジェクトを設定するには、サンプルを参照してください。

Example

example.dart ライブラリに、JsonSerializable アノテーションが付けられた Person クラスがあるとします:

import 'package:json_annotation/json_annotation.dart';

part 'example.g.dart';

// flutter pub run build_runner watch --delete-conflicting-outputs

()
class Person {
  /// The generated code assumes these values exist in JSON.
  final String firstName, lastName;

  /// The generated code below handles if the corresponding JSON value doesn't
  /// exist or is empty.
  final DateTime? dateOfBirth;

  Person({required this.firstName, required this.lastName, this.dateOfBirth});

  /// Connect the generated [_$PersonFromJson] function to the `fromJson`
  /// factory.
  factory Person.fromJson(Map<String, dynamic> json) => _$PersonFromJson(json);

  /// Connect the generated [_$PersonToJson] function to the `toJson` method.
  Map<String, dynamic> toJson() => _$PersonToJson(this);
}

freezedよりは少ないな😅

内部実装
// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'example.dart';

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

Person _$PersonFromJson(Map<String, dynamic> json) => Person(
      firstName: json['firstName'] as String,
      lastName: json['lastName'] as String,
      dateOfBirth: json['dateOfBirth'] == null
          ? null
          : DateTime.parse(json['dateOfBirth'] as String),
    );

Map<String, dynamic> _$PersonToJson(Person instance) => <String, dynamic>{
      'firstName': instance.firstName,
      'lastName': instance.lastName,
      'dateOfBirth': instance.dateOfBirth?.toIso8601String(),
    };

Running the code generator

コードにアノテーションを追加したら、次にコードジェネレーターを実行して、不足している .g.dart 生成用 dart ファイルを生成する必要があります。

パッケージ・ディレクトリで dart run build_runner build を実行します。

Annotation values

このパッケージを使用するために必要なアノテーションは JsonSerializable だけです。正しく設定されたパッケージ内の)クラスに適用すると、ビルド時にtoJsonとfromJsonのコードが生成されます。コードがどのように生成されるかを制御するには、3つの方法があります:

  1. ターゲット・フィールドをアノテートする JsonKey にプロパティを設定する。
  2. ターゲット・タイプをアノテートする JsonSerializable にプロパティを設定する。
  3. build.yamlに設定を追加する。

すべての JsonSerializable フィールドは build.yaml で設定できます。すべてのクラスもしくはほとんどのクラスで同じ設定をしたい場合、YAML ファイルで値を指定するほうが簡単です。JsonSerializable で明示的に設定された値は build.yaml での設定よりも優先されます。

JsonKey の設定と JsonSerializable の設定には重複があります。このような場合、JsonSerializable に設定された値よりも JsonKey に設定されたプロパティが優先されます。

公式の解説長いのでこれぐらいにしておこう📚

JsonSerializableとはなんなのか?

クラスの上についてる@(アノテーション)ですね。

()
class Person {
内部実装長い長い
// Copyright (c) 2017, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:meta/meta_meta.dart';

import 'allowed_keys_helpers.dart';
import 'checked_helpers.dart';
import 'enum_helpers.dart';
import 'json_converter.dart';
import 'json_key.dart';

part 'json_serializable.g.dart';

/// Values for the automatic field renaming behavior for [JsonSerializable].
enum FieldRename {
  /// Use the field name without changes.
  none,

  /// Encodes a field named `kebabCase` with a JSON key `kebab-case`.
  kebab,

  /// Encodes a field named `snakeCase` with a JSON key `snake_case`.
  snake,

  /// Encodes a field named `pascalCase` with a JSON key `PascalCase`.
  pascal,

  /// Encodes a field named `screamingSnakeCase` with a JSON key
  /// `SCREAMING_SNAKE_CASE`
  screamingSnake,
}

/// An annotation used to specify a class to generate code for.
(
  checked: true,
  disallowUnrecognizedKeys: true,
  fieldRename: FieldRename.snake,
)
({TargetKind.classType})
class JsonSerializable {
  /// If `true`, [Map] types are *not* assumed to be [Map<String, dynamic>]
  /// – which is the default type of [Map] instances return by JSON decode in
  /// `dart:convert`.
  ///
  /// This will increase the code size, but allows [Map] types returned
  /// from other sources, such as `package:yaml`.
  ///
  /// *Note: in many cases the key values are still assumed to be [String]*.
  final bool? anyMap;

  /// If `true`, generated `fromJson` functions include extra checks to validate
  /// proper deserialization of types.
  ///
  /// If an exception is thrown during deserialization, a
  /// [CheckedFromJsonException] is thrown.
  final bool? checked;

  /// Specifies a named constructor to target when creating the `fromJson`
  /// function.
  ///
  /// If the value is not set or an empty [String], the default constructor
  /// is used.
  ///
  /// This setting has no effect if [createFactory] is `false`.
  final String? constructor;

  /// If `true` (the default), a private, static `_$ExampleFromJson` method
  /// is created in the generated part file.
  ///
  /// Call this method from a factory constructor added to the source class:
  ///
  /// ```dart
  /// @JsonSerializable()
  /// class Example {
  ///   // ...
  ///   factory Example.fromJson(Map<String, dynamic> json) =>
  ///     _$ExampleFromJson(json);
  /// }
  /// ```
  final bool? createFactory;

  /// If `true` (defaults to false), a private, static `_$ExampleJsonMeta`
  /// constant is created in the generated part file.
  ///
  /// This constant can be used by other code-generators to support features
  /// such as [fieldRename].
  final bool? createFieldMap;

  /// If `true` (defaults to false), a private class `_$ExampleJsonKeys`
  /// class is created in the generated part file.
  ///
  /// This class will contain every property as a [String] field with the JSON
  /// key as the value.
  ///
  /// ```dart
  /// @JsonSerializable(createJsonKeys: true)
  /// class Example {
  ///   @JsonKey(name: 'LAST_NAME')
  ///   String? firstName;
  ///
  ///   // Will have the value `LAST_NAME`
  ///   static const firstName = _$ExampleJsonKeys.firstName;
  /// }
  /// ```
  final bool? createJsonKeys;

  /// If `true` (defaults to false), a private, static `_$ExamplePerFieldToJson`
  /// abstract class will be generated in the part file.
  ///
  /// This abstract class will contain one static function per property,
  /// exposing a way to encode only this property instead of the entire object.
  final bool? createPerFieldToJson;

  /// If `true` (the default), A top-level function is created that you can
  /// reference from your class.
  ///
  /// ```dart
  /// @JsonSerializable()
  /// class Example {
  ///   Map<String, dynamic> toJson() => _$ExampleToJson(this);
  /// }
  /// ```
  final bool? createToJson;

  /// If `false` (the default), then the generated `FromJson` function will
  /// ignore unrecognized keys in the provided JSON [Map].
  ///
  /// If `true`, unrecognized keys will cause an [UnrecognizedKeysException] to
  /// be thrown.
  final bool? disallowUnrecognizedKeys;

  /// If `true`, generated `toJson` methods will explicitly call `toJson` on
  /// nested objects.
  ///
  /// When using JSON encoding support in `dart:convert`, `toJson` is
  /// automatically called on objects, so the default behavior
  /// (`explicitToJson: false`) is to omit the `toJson` call.
  ///
  /// Example of `explicitToJson: false` (default)
  ///
  /// ```dart
  /// Map<String, dynamic> toJson() => {'child': child};
  /// ```
  ///
  /// Example of `explicitToJson: true`
  ///
  /// ```dart
  /// Map<String, dynamic> toJson() => {'child': child?.toJson()};
  /// ```
  final bool? explicitToJson;

  /// Defines the automatic naming strategy when converting class field names
  /// into JSON map keys.
  ///
  /// With a value [FieldRename.none] (the default), the name of the field is
  /// used without modification.
  ///
  /// See [FieldRename] for details on the other options.
  ///
  /// Note: the value for [JsonKey.name] takes precedence over this option for
  /// fields annotated with [JsonKey].
  final FieldRename? fieldRename;

  /// When `true` on classes with type parameters (generic types), extra
  /// "helper" parameters will be generated for `fromJson` and/or `toJson` to
  /// support serializing values of those types.
  ///
  /// For example, the generated code for
  ///
  /// ```dart
  /// @JsonSerializable(genericArgumentFactories: true)
  /// class Response<T> {
  ///   int status;
  ///   T value;
  /// }
  /// ```
  ///
  /// Looks like
  ///
  /// ```dart
  /// Response<T> _$ResponseFromJson<T>(
  ///   Map<String, dynamic> json,
  ///   T Function(Object json) fromJsonT,
  /// ) {
  ///   return Response<T>()
  ///     ..status = (json['status'] as num).toInt()
  ///     ..value = fromJsonT(json['value']);
  /// }
  ///
  /// Map<String, dynamic> _$ResponseToJson<T>(
  ///   Response<T> instance,
  ///   Object Function(T value) toJsonT,
  /// ) =>
  ///     <String, dynamic>{
  ///       'status': instance.status,
  ///       'value': toJsonT(instance.value),
  ///     };
  /// ```
  ///
  /// Notes:
  ///
  /// 1. This option has no effect on classes without type parameters.
  ///    If used on such a class, a warning is echoed in the build log.
  /// 1. If this option is set for all classes in a package via `build.yaml`
  ///    it is only applied to classes with type parameters – so no warning is
  ///    echoed.
  final bool? genericArgumentFactories;

  /// When `true`, only fields annotated with [JsonKey] will have code
  /// generated.
  ///
  /// It will have the same effect as if those fields had been annotated with
  /// [JsonKey.includeToJson] and [JsonKey.includeFromJson] set to `false`
  final bool? ignoreUnannotated;

  /// Whether the generator should include fields with `null` values in the
  /// serialized output.
  ///
  /// If `true` (the default), all fields are written to JSON, even if they are
  /// `null`.
  ///
  /// If a field is annotated with `JsonKey` with a non-`null` value for
  /// `includeIfNull`, that value takes precedent.
  final bool? includeIfNull;

  /// A list of [JsonConverter] to apply to this class.
  ///
  /// Writing:
  ///
  /// ```dart
  /// @JsonSerializable(converters: [MyJsonConverter()])
  /// class Example {...}
  /// ```
  ///
  /// is equivalent to writing:
  ///
  /// ```dart
  /// @JsonSerializable()
  /// @MyJsonConverter()
  /// class Example {...}
  /// ```
  ///
  /// The main difference is that this allows reusing a custom
  /// [JsonSerializable] over multiple classes:
  ///
  /// ```dart
  /// const myCustomAnnotation = JsonSerializable(
  ///   converters: [MyJsonConverter()],
  /// );
  ///
  /// @myCustomAnnotation
  /// class Example {...}
  ///
  /// @myCustomAnnotation
  /// class Another {...}
  /// ```
  (includeFromJson: false, includeToJson: false)
  final List<JsonConverter>? converters;

  /// Creates a new [JsonSerializable] instance.
  const JsonSerializable({
    ('Has no effect') bool? nullable,
    this.anyMap,
    this.checked,
    this.constructor,
    this.createFieldMap,
    this.createJsonKeys,
    this.createFactory,
    this.createToJson,
    this.disallowUnrecognizedKeys,
    this.explicitToJson,
    this.fieldRename,
    this.ignoreUnannotated,
    this.includeIfNull,
    this.converters,
    this.genericArgumentFactories,
    this.createPerFieldToJson,
  });

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

  /// An instance of [JsonSerializable] with all fields set to their default
  /// values.
  ('Was only ever included to support builder infrastructure.')
  static const defaults = JsonSerializable(
    anyMap: false,
    checked: false,
    constructor: '',
    createFactory: true,
    createToJson: true,
    disallowUnrecognizedKeys: false,
    explicitToJson: false,
    fieldRename: FieldRename.none,
    ignoreUnannotated: false,
    includeIfNull: true,
    genericArgumentFactories: false,
  );

  /// Returns a new [JsonSerializable] instance with fields equal to the
  /// corresponding values in `this`, if not `null`.
  ///
  /// Otherwise, the returned value has the default value as defined in
  /// [defaults].
  ('Was only ever included to support builder infrastructure.')
  JsonSerializable withDefaults() => JsonSerializable(
        anyMap: anyMap ?? defaults.anyMap,
        checked: checked ?? defaults.checked,
        constructor: constructor ?? defaults.constructor,
        createFactory: createFactory ?? defaults.createFactory,
        createToJson: createToJson ?? defaults.createToJson,
        disallowUnrecognizedKeys:
            disallowUnrecognizedKeys ?? defaults.disallowUnrecognizedKeys,
        explicitToJson: explicitToJson ?? defaults.explicitToJson,
        fieldRename: fieldRename ?? defaults.fieldRename,
        ignoreUnannotated: ignoreUnannotated ?? defaults.ignoreUnannotated,
        includeIfNull: includeIfNull ?? defaults.includeIfNull,
        genericArgumentFactories:
            genericArgumentFactories ?? defaults.genericArgumentFactories,
      );

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

@JsonSerializable()
このアノテーションを使用することで、Dart クラスを JSON にシリアライズおよびデシリアライズするためのコードを自動生成できます。

解説すると

シリアライズ(serialize)とデシリアライズ(deserialize)は、データ構造やオブジェクトを扱う際の重要な概念です。簡単に説明すると:

  1. シリアライズ(直列化):

    • オブジェクトや複雑なデータ構造を、保存や転送が可能な形式(通常はバイト列や文字列)に変換すること。
    • 例: JavaScriptオブジェクトをJSONに変換する。
  2. デシリアライズ(逆直列化):

    • シリアライズされたデータを元のオブジェクトや構造に戻すこと。
    • 例: JSONからJavaScriptオブジェクトを再構築する。

このコンテキストでは、JsonSerializableクラスはDartオブジェクトとJSONの間のシリアライズとデシリアライズを容易にするためのツールです。これにより、Dartクラスのインスタンスを簡単にJSONに変換したり、JSONからDartオブジェクトを作成したりすることができます。

主な特徴として:

  • toJsonメソッド: オブジェクトをJSONに変換(シリアライズ)
  • fromJsonファクトリメソッド: JSONからオブジェクトを作成(デシリアライズ)
  • フィールドの自動命名規則の設定
  • 型チェックやバリデーションのオプション
  • カスタムコンバーターの適用

これらの機能により、開発者はデータの永続化や転送を効率的に行えるようになります。

まとめ

freezedを使う時によく使ってたりしますが、やってくれてたことは、オブジェクトのJSON化、JSONをオブジェクト化するものみたいですね。解説は長かったが、やってることは単純でしたね、

Discussion