parse_json

License Pub.dev Github Stars

Type-safe JSON deserialization

NO CODE GENERATION OR REFLECTION

parse_json uses named parameters from your constructor to match JSON keys. It supports many features such as optionals, nested types, polymorphism/inheritance, enums, and collections. It is also type-safe, meaning that if you try to deserialize a JSON object into a type that doesn't match, it will throw an error.

Getting Started

Add the package to your pubspec.yaml:

For Flutter projects:

flutter pub add parse_json

For Dart projects:

dart pub add parse_json

Then import the package in your Dart code:

import 'package:parse_json/parse_json.dart';

Then match up a constructor's named parameters with your JSON data, and make a factory function that uses parse to create an object from JSON. In the parse function, you will need to provide the constructor to use, the JSON data, and a map that matches the named parameters of the constructor to the JSON keys. Here is an example of a simple class with primitive members:

Data:

{
  "myString": "exampleStr",
  "myDouble": 12.5
}

Dart Code:

import 'package:parse_json/parse_json.dart';

final class ExampleObject {
  final String myString;
  final double myDouble;

  /// This is the constructor that will be called by `parse`
  const ExampleObject({
    required this.myString,
    required this.myDouble,
  });

  /// This is the factory function that will be used to parse the JSON
  factory ExampleObject.fromJson(dynamic json) => 
    parse(
      ExampleObject.new, // the constructor to use
      json,
      // note: the keys in this map MUST match the named parameters of the constructor
      {
        'myString': string,
        'myDouble': float,
      },
    );
}

For strings, you will use string, for doubles you will use float, for ints you will use integer, and for bools you will use boolean.

You do not need to name your member variables the same as the JSON keys. You can specify a special constructor with named parameters that match the JSON keys, and use whatever names you like for your member variables. You also do not need to parse every member variable from JSON, in cases where you have derived data. Your members can also be non-final, but most of the examples in this documentation use final members for consistency.

Data:

{
  "myString": "exampleStr",
  "myDouble": 12.5
}

Dart Code:

import 'package:parse_json/parse_json.dart';

final class ExampleObject {
  /// Members with names different from those in the constructor
  final String name;
  final double originalHeight;

  /// Non-final member
  double currentHeight;

  /// Constructor that will be called by `parse`
  const ExampleObject.json({
    required String myString,
    required double myDouble,
  }) : name = myString,
       originalHeight = myDouble,
       currentHeight = myDouble;

  /// Constructor we use for other stuff
  const ExampleObject(this.name, this.originalHeight, this.currentHeight);

  /// Factory function for parsing and constructing from JSON
  factory ExampleObject.fromJson(dynamic json) => 
    parse(
      ExampleObject.json, // the constructor to use
      json,
      {
        'myString': string,
        'myDouble': float,
      },
    );
}

You will need to create the map parameter in parse a little differently when you have a JSON property that is user-defined (Ones that are not String, int, double, bool, or some List or Map of those). Here is an example of a class with user-defined members:

Data:

{
  "myBoolList": [false, true, false],
  "myIntList": [12.5, 10.0, 5.0],
  "myFriend": {
    "myString": "friendStr",
    "myDouble": 10.5
  },
  "myOptionalFriend": ... another friend or this field could be omitted
  "friendList": [
    ... more friends
  ]
}

Dart Code:

import 'package:parse_json/parse_json.dart';

final class ExampleObject {
  final String myString;
  final double myDouble;

  const ExampleObject({
    required this.myString,
    required this.myDouble,
  });

  factory ExampleObject.fromJson(dynamic json) => 
    parse(
      ExampleObject.new,
      json,
      {
        'myString': string,
        'myDouble': float,
      },
    );
}

final class ComplexExampleObject {
  final List<bool> myBoolList;
  final List<int> myIntList;
  final ExampleObject myFriend;
  final ExampleObject? myOptionalFriend;
  final List<ExampleObject> friendList;

  const ComplexExampleObject({
    required this.myBoolList,
    required this.myIntList,
    required this.myFriend,
    required this.friendList,
    this.myOptionalFriend,
  });

  factory ComplexExampleObject.fromJson(dynamic json) => 
    parse(
      ComplexExampleObject.new,
      json,
      {
        'myBoolList': boolean.list,
        'myIntList': integer.list,
        'myFriend': ExampleObject.fromJson.required,
        'myOptionalFriend': ExampleObject.fromJson.optional,
        'friendList': ExampleObject.fromJson.list,
      },
    );
}

Example:

Primitive types:

If you're defining a primitive property (such as String, int, double, bool, or some List or Map of one of those), you can use the a constant for your property. Here is an example of a simple class that only has primitive members:

import 'package:parse_json/parse_json.dart';

final class ExampleObject {
  final String myString;
  final double myDouble;
  final int myInt;
  final bool myBool;
  final String? myOptionalString;
  final int? myOptionalInt;

  const ExampleObject({
    required this.myString,
    required this.myDouble,
    required this.myInt,
    required this.myBool,
    this.myOptionalString,
    this.myOptionalInt,
  });

  factory ExampleObject.fromJson(dynamic json) => 
    parse(
      ExampleObject.new,
      json,
      {
        'myString': string,
        'myDouble': float,
        'myInt': integer,
        'myBool': boolean,
        'myOptionalString': string.optional,
        'myOptionalInt': integer.optional,
      }
    );
}

User-defined types:

For non-primitive types (user-defined types), you must use the .required, .optional, .list, .map, .stringMap, or .intMap methods on the user-defined type's fromJson factory function.

import 'package:parse_json/parse_json.dart';

final class SimpleObject {
  final String myString;
  final double myDouble;

  const SimpleObject({
    required this.myString,
    required this.myDouble,
  });

  factory SimpleObject.fromJson(dynamic json) => 
    parse(
      SimpleObject.new,
      json,
      {
        'myString': string,
        'myDouble': float,
      }
    );
}

final class ComplexObject {
  final List<SimpleObject> exampleList;
  final Map<String, SimpleObject> exampleMap;
  final SimpleObject? optionalExampleObject;
  final SimpleObject exampleObject;

  const ComplexObject({
    required this.exampleList,
    required this.exampleMap,
    required this.exampleObject,
    this.optionalExampleObject,
  });

  factory ComplexObject.fromJson(dynamic json) => 
    parse(
      ComplexObject.new,
      json,
      {
        'exampleList': SimpleObject.fromJson.list,
        'exampleMap': SimpleObject.fromJson.stringMap,
        'exampleObject': SimpleObject.fromJson.required,
        'optionalExampleObject': SimpleObject.fromJson.optional,
      }
    );
}

Enums

Enums are pretty much the same as user-defined types. You need to provide a fromJson factory function for the enum. You can use the switch function to match the JSON value to the enum value. Here is an example of a class with an enum member:

enum ExampleEnum {
  a,
  b,
  c;

  factory ExampleEnum.fromJson(dynamic json) => switch (json) {
        'abbracaddabra' => ExampleEnum.a,
        'bye-bye' => ExampleEnum.b,
        'ciao' => ExampleEnum.c,
        _ => throw Exception('Unknown enum value')
      };
}

final class ObjectWithEnums {
  final ExampleEnum a;
  final ExampleEnum? b;
  final Map<String, List<ExampleEnum>>? c;

  const ObjectWithEnums({
    required this.a,
    this.b,
    this.c,
  }) : super();

  factory ObjectWithEnums.fromJson(dynamic json) =>
      parse(
        ObjectWithEnums.new, 
        json, 
        {
          'a': ExampleEnum.fromJson.required,
          'b': ExampleEnum.fromJson.optional,
          'c': ExampleEnum.fromJson.list.stringMap.optional,
        }
      );
}

Default/Fallback values

You can provide a default/fallback value for a member by using .withDefault on a JSON property. If the JSON you are parsing is missing a property, the default/fallback will be used. Here is an example of a class with default values:

final class SimpleDefaults {
  final String myString;
  final double myDouble;
  final int myInt;
  final bool myBool;

  const SimpleDefaults({
    required this.myString,
    required this.myInt,
    required this.myDouble,
    required this.myBool,
  }) : super();

  factory SimpleDefaults.fromJson(dynamic json) =>
    parse(
      SimpleDefaults.new, 
      json, 
      {
        'myString': string,
        'myDouble': float.withDefault(12.5),
        'myInt': integer,
        'myBool': boolean.withDefault(true),
      },
    );
}

final class ComplexDefaults {
  final SimpleDefaults object;
  final List<bool> boolList;

  const ComplexDefaults({
    required this.object,
    required this.boolList,
  }) : super();

  factory ComplexDefaults.fromJson(dynamic json) =>
    parse(
      ComplexDefaults.new, 
      json, 
      {
        'object': SimpleDefaults.fromJson.withDefault(
          SimpleDefaults(
            myString: 'defaultStr',
            myInt: -1,
            myDouble: -100.5,
            myBool: false,
          ),
        ),
        'boolList': boolean.list.withDefault([false, true, false])
      },
    );
}

Collections

For collections, you can use the .list, .map, .stringMap, or .intMap methods on any JsonProperty. Here is an example of a class with a list and a map:

import 'package:parse_json/parse_json.dart';

final class ExampleObject {
  final List<String> myStringList;
  final Map<String, double> myStringDoubleMap;

  const ExampleObject({
    required this.myStringList,
    required this.myStringDoubleMap,
  });

  factory ExampleObject.fromJson(dynamic json) => 
    parse(
      ExampleObject.new,
      json,
      {
        'myStringList': string.list,
        'myStringDoubleMap': float.stringMap,
      }
    );
}

Inheritance/Polymorphic types:

With polymorphic base types, you need to use polymorphicParse. You will need to provide a polymorphicKey for the base class and a unique id for each subclass. The polymorphicKey is the key in the JSON that will be used to determine the type of the object. The unique ids are the values of polymorphicKey that will be used to determine the type of an object polymorphically at runtime. You must provide the fromJson factory functions to the derivedTypes parameter of polymorphicParse, and use a unique id for each subclass as the key in the map. You can also provide a baseDefinition to the polymorphicParse function that will be used to parse the base class if it is not abstract and polymorphicKey is missing from the JSON.

import 'package:parse_json/parse_json.dart';

final class BaseClass {
  static const polymorphicKey = 'type';
  
  final String myString;
  final double myDouble;

  const BaseClass({
    required this.myString,
    required this.myDouble,
  });

  factory BaseClass.fromJson(dynamic json) => 
    polymorphicParse(
      polymorphicKey,
      json,
      {
        SubClassA.polymorphicId: SubClassA.fromJson,
        SubClassB.polymorphicId: SubClassB.fromJson,
      },
      baseDefinition: DefinedType(BaseClass.new, {
        'myString': string,
        'myDouble': float,
    }));
}

final class SubClassA extends BaseClass {
  static const polymorphicId = 'A';

  final int myInt;

  const SubClassA({
    required super.myString,
    required super.myDouble,
    required this.myInt,
  }) : super();

  factory SubClassA.fromJson(dynamic json) => 
    parse(
      SubClassA.new,
      json,
      {
        'myString': string,
        'myDouble': float,
        'myInt': integer,
      },
    );
}

final class SubClassB extends BaseClass {
  static const polymorphicId = 'B';

  final ExampleObject myExampleObject;

  const SubClassB({
    required super.myString,
    required super.myDouble,
    required this.myExampleObject,
  }) : super();

  factory SubClassB.fromJson(dynamic json) => 
    parse(
      SubClassB.fromJson,
      json,
      {
        'myString': string,
        'myDouble': float,
        'myExampleObject': ExampleObject.fromJson.required,
      },
    );
}

License/Disclaimer

See LICENSE

Libraries

parse_json