parse_json
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
- User-defined types
- Enums
- Default/Fallback values
- Collections
- Inheritance/Polymorphic types
- Full 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