Added json_god package

This commit is contained in:
thomashii 2021-03-07 23:56:09 +08:00
parent 6661055410
commit 0752498a16
15 changed files with 801 additions and 0 deletions

View file

@ -0,0 +1,8 @@
# 2.0.0-beta+3
* Long-needed updates, ensured Dart 2 compatibility, fixed DDC breakages.
* Patches for reflection bugs with typing.
# 2.0.0-beta+2
* This version breaks in certain Dart versions (likely anything *after* `2.0.0-dev.59.0`)
until https://github.com/dart-lang/sdk/issues/33594 is resolved.
* Removes the reference to `Schema` class.

21
packages/json_god/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2016 Tobe O
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

113
packages/json_god/README.md Normal file
View file

@ -0,0 +1,113 @@
# JSON God v2
[![Pub](https://img.shields.io/pub/v/json_god.svg)](https://pub.dartlang.org/packages/json_god)
[![build status](https://travis-ci.org/thosakwe/json_god.svg)](https://travis-ci.org/thosakwe/json_god)
The ***new and improved*** definitive solution for JSON in Dart.
# Installation
dependencies:
json_god: ^2.0.0-beta
# Usage
It is recommended to import the library under an alias, i.e., `god`.
```dart
import 'package:json_god/json_god.dart' as god;
```
## Serializing JSON
Simply call `god.serialize(x)` to synchronously transform an object into a JSON
string.
```dart
Map map = {"foo": "bar", "numbers": [1, 2, {"three": 4}]};
// Output: {"foo":"bar","numbers":[1,2,{"three":4]"}
String json = god.serialize(map);
print(json);
```
You can easily serialize classes, too. JSON God also supports classes as members.
```dart
class A {
String foo;
A(this.foo);
}
class B {
String hello;
A nested;
B(String hello, String foo) {
this.hello = hello;
this.nested = new A(foo);
}
}
main() {
God god = new God();
print(god.serialize(new B("world", "bar")));
}
// Output: {"hello":"world","nested":{"foo":"bar"}}
```
If a class has a `toJson` method, it will be called instead.
## Deserializing JSON
Deserialization is equally easy, and is provided through `god.deserialize`.
```dart
Map map = god.deserialize('{"hello":"world"}');
int three = god.deserialize("3");
```
### Deserializing to Classes
JSON God lets you deserialize JSON into an instance of any type. Simply pass the
type as the second argument to `god.deserialize`.
If the class has a `fromJson` constructor, it will be called instead.
```dart
class Child {
String foo;
}
class Parent {
String hello;
Child child = new Child();
}
main() {
God god = new God();
Parent parent = god.deserialize('{"hello":"world","child":{"foo":"bar"}}', Parent);
print(parent);
}
```
**Any JSON-deserializable classes must initializable without parameters.
If `new Foo()` would throw an error, then you can't use Foo with JSON.**
This allows for validation of a sort, as only fields you have declared will be
accepted.
```dart
class HasAnInt { int theInt; }
HasAnInt invalid = god.deserialize('["some invalid input"]', HasAnInt);
// Throws an error
```
An exception will be thrown if validation fails.
# Thank you for using JSON God
Thank you for using this library. I hope you like it.
Feel free to follow me on Twitter:
[@thosakwe](http://twitter.com/thosakwe)
Or, check out [my blog](https://thosakwe.com)

View file

@ -0,0 +1,3 @@
analyzer:
strong-mode:
implicit-casts: false

View file

@ -0,0 +1,17 @@
/// A robust library for JSON serialization and deserialization.
library json_god;
import 'package:dart2_constant/convert.dart';
import 'package:logging/logging.dart';
import 'src/reflection.dart' as reflection;
part 'src/serialize.dart';
part 'src/deserialize.dart';
part 'src/validation.dart';
part 'src/util.dart';
/// Instead, listen to [logger].
@deprecated
bool debug = false;
final Logger logger = new Logger('json_god');

View file

@ -0,0 +1,43 @@
part of json_god;
/// Deserializes a JSON string into a Dart datum.
///
/// You can also provide an output Type to attempt to serialize the JSON into.
deserialize(String json, {Type outputType}) {
var deserialized = deserializeJson(json, outputType: outputType);
logger.info("Deserialization result: $deserialized");
return deserialized;
}
/// Deserializes JSON into data, without validating it.
deserializeJson(String s, {Type outputType}) {
logger.info("Deserializing the following JSON: $s");
if (outputType == null) {
logger.info("No output type was specified, so we are just using json.decode");
return json.decode(s);
} else {
logger.info("Now deserializing to type: $outputType");
return deserializeDatum(json.decode(s), outputType: outputType);
}
}
/// Deserializes some JSON-serializable value into a usable Dart value.
deserializeDatum(value, {Type outputType}) {
if (outputType != null) {
return reflection.deserialize(value, outputType, deserializeDatum);
} else if (value is List) {
logger.info("Deserializing this List: $value");
return value.map(deserializeDatum).toList();
} else if (value is Map) {
logger.info("Deserializing this Map: $value");
Map result = {};
value.forEach((k, v) {
result[k] = deserializeDatum(v);
});
return result;
} else if (_isPrimitive(value)) {
logger.info("Value $value is a primitive");
return value;
}
}

View file

@ -0,0 +1,191 @@
library json_god.reflection;
import 'dart:mirrors';
import 'package:json_god/json_god.dart';
const Symbol hashCodeSymbol = #hashCode;
const Symbol runtimeTypeSymbol = #runtimeType;
typedef Serializer(value);
typedef Deserializer(value, {Type outputType});
List<Symbol> _findGetters(ClassMirror classMirror) {
List<Symbol> result = [];
classMirror.instanceMembers
.forEach((Symbol symbol, MethodMirror methodMirror) {
if (methodMirror.isGetter &&
symbol != hashCodeSymbol &&
symbol != runtimeTypeSymbol) {
logger.info("Found getter on instance: $symbol");
result.add(symbol);
}
});
return result;
}
serialize(value, Serializer serializer, [@deprecated bool debug = false]) {
logger.info("Serializing this value via reflection: $value");
Map result = {};
InstanceMirror instanceMirror = reflect(value);
ClassMirror classMirror = instanceMirror.type;
// Check for toJson
for (Symbol symbol in classMirror.instanceMembers.keys) {
if (symbol == #toJson) {
logger.info("Running toJson...");
var result = instanceMirror.invoke(symbol, []).reflectee;
logger.info("Result of serialization via reflection: $result");
return result;
}
}
for (Symbol symbol in _findGetters(classMirror)) {
String name = MirrorSystem.getName(symbol);
var valueForSymbol = instanceMirror.getField(symbol).reflectee;
try {
result[name] = serializer(valueForSymbol);
logger.info("Set $name to $valueForSymbol");
} catch (e, st) {
logger.severe("Could not set $name to $valueForSymbol", e, st);
}
}
logger.info("Result of serialization via reflection: $result");
return result;
}
deserialize(value, Type outputType, Deserializer deserializer,
[@deprecated bool debug = false]) {
logger.info("About to deserialize $value to a $outputType");
try {
if (value is List) {
List<TypeMirror> typeArguments = reflectType(outputType).typeArguments;
Iterable it;
if (typeArguments.isEmpty) {
it = value.map(deserializer);
} else {
it = value.map((item) =>
deserializer(item, outputType: typeArguments[0].reflectedType));
}
if (typeArguments.isEmpty) return it.toList();
logger.info('Casting list elements to ${typeArguments[0]
.reflectedType} via List.from');
var mirror = reflectType(List, [typeArguments[0].reflectedType]);
if (mirror is ClassMirror) {
var output = mirror.newInstance(#from, [it]).reflectee;
logger.info('Casted list type: ${output.runtimeType}');
return output;
} else {
throw new ArgumentError(
'${typeArguments[0].reflectedType} is not a class.');
}
} else if (value is Map)
return _deserializeFromJsonByReflection(value, deserializer, outputType);
else
return deserializer(value);
} catch (e, st) {
logger.severe('Deserialization failed.', e, st);
rethrow;
}
}
/// Uses mirrors to deserialize an object.
_deserializeFromJsonByReflection(
data, Deserializer deserializer, Type outputType,
[@deprecated bool debug = false]) {
// Check for fromJson
var typeMirror = reflectType(outputType);
if (typeMirror is! ClassMirror) {
throw new ArgumentError('$outputType is not a class.');
}
var type = typeMirror as ClassMirror;
var fromJson =
new Symbol('${MirrorSystem.getName(type.simpleName)}.fromJson');
for (Symbol symbol in type.declarations.keys) {
if (symbol == fromJson) {
var decl = type.declarations[symbol];
if (decl is MethodMirror && decl.isConstructor) {
logger.info("Running fromJson...");
var result = type.newInstance(#fromJson, [data]).reflectee;
logger.info("Result of deserialization via reflection: $result");
return result;
}
}
}
ClassMirror classMirror = type;
InstanceMirror instanceMirror = classMirror.newInstance(new Symbol(""), []);
if (classMirror.isSubclassOf(reflectClass(Map))) {
var typeArguments = classMirror.typeArguments;
if (typeArguments.isEmpty ||
classMirror.typeArguments
.every((t) => t == currentMirrorSystem().dynamicType)) {
return data;
} else {
var mapType =
reflectType(Map, typeArguments.map((t) => t.reflectedType).toList())
as ClassMirror;
logger.info('Casting this map $data to Map of [$typeArguments]');
var output = mapType.newInstance(new Symbol(''), []).reflectee;
for (var key in data.keys) {
output[key] = data[key];
}
logger.info('Output: $output of type ${output.runtimeType}');
return output;
}
} else {
data.keys.forEach((key) {
try {
logger.info("Now deserializing value for $key");
logger.info("data[\"$key\"] = ${data[key]}");
var deserializedValue = deserializer(data[key]);
logger.info("I want to set $key to the following ${deserializedValue
.runtimeType}: $deserializedValue");
// Get target type of getter
Symbol searchSymbol = new Symbol(key.toString());
Symbol symbolForGetter = classMirror.instanceMembers.keys
.firstWhere((x) => x == searchSymbol);
Type requiredType = classMirror
.instanceMembers[symbolForGetter].returnType.reflectedType;
if (data[key].runtimeType != requiredType) {
logger.info("Currently, $key is a ${data[key].runtimeType}.");
logger.info("However, $key must be a $requiredType.");
deserializedValue =
deserializer(deserializedValue, outputType: requiredType);
}
logger.info(
"Final deserialized value for $key: $deserializedValue <${deserializedValue
.runtimeType}>");
instanceMirror.setField(new Symbol(key.toString()), deserializedValue);
logger.info("Success! $key has been set to $deserializedValue");
} catch (e, st) {
logger.severe('Could not set value for field $key.', e, st);
}
});
}
return instanceMirror.reflectee;
}

View file

@ -0,0 +1,35 @@
part of json_god;
/// Serializes any arbitrary Dart datum to JSON. Supports schema validation.
String serialize(value) {
var serialized = serializeObject(value);
logger.info('Serialization result: $serialized');
return json.encode(serialized);
}
/// Transforms any Dart datum into a value acceptable to json.encode.
serializeObject(value) {
if (_isPrimitive(value)) {
logger.info("Serializing primitive value: $value");
return value;
} else if (value is DateTime) {
logger.info("Serializing this DateTime: $value");
return value.toIso8601String();
} else if (value is Iterable) {
logger.info("Serializing this Iterable: $value");
return value.map(serializeObject).toList();
} else if (value is Map) {
logger.info("Serializing this Map: $value");
return serializeMap(value);
} else
return serializeObject(reflection.serialize(value, serializeObject));
}
/// Recursively transforms a Map and its children into JSON-serializable data.
Map serializeMap(Map value) {
Map outputMap = {};
value.forEach((key, value) {
outputMap[key] = serializeObject(value);
});
return outputMap;
}

View file

@ -0,0 +1,5 @@
part of json_god;
bool _isPrimitive(value) {
return value is num || value is bool || value is String || value == null;
}

View file

@ -0,0 +1,25 @@
part of json_god;
/// Thrown when schema validation fails.
class JsonValidationError implements Exception {
//final Schema schema;
final invalidData;
final String cause;
const JsonValidationError(
String this.cause, this.invalidData);//, Schema this.schema);
}
/// Specifies a schema to validate a class with.
class WithSchema {
final Map schema;
const WithSchema(Map this.schema);
}
/// Specifies a schema to validate a class with.
class WithSchemaUrl {
final String schemaUrl;
const WithSchemaUrl(String this.schemaUrl);
}

View file

@ -0,0 +1,14 @@
name: json_god
version: 3.0.0
authors:
- Tobe O <thosakwe@gmail.com>
description: Easy JSON serialization and deserialization in Dart.
homepage: https://github.com/thosakwe/json_god
environment:
sdk: ">=2.10.0 <3.0.0"
dependencies:
dart2_constant: ^1.0.0
logging: ^1.0.0
dev_dependencies:
stack_trace: ^1.0.0
test: any

View file

@ -0,0 +1,112 @@
import 'package:json_god/json_god.dart' as god;
import 'package:test/test.dart';
import 'shared.dart';
main() {
god.logger.onRecord.listen(printRecord);
group('deserialization', () {
test('deserialize primitives', testDeserializationOfPrimitives);
test('deserialize maps', testDeserializationOfMaps);
test('deserialize maps + reflection', testDeserializationOfMapsWithReflection);
test('deserialize lists + reflection',
testDeserializationOfListsAsWellAsViaReflection);
test('deserialize with schema validation',
testDeserializationWithSchemaValidation);
});
}
testDeserializationOfPrimitives() {
expect(god.deserialize('1'), equals(1));
expect(god.deserialize('1.4'), equals(1.4));
expect(god.deserialize('"Hi!"'), equals("Hi!"));
expect(god.deserialize("true"), equals(true));
expect(god.deserialize("null"), equals(null));
}
testDeserializationOfMaps() {
String simpleJson =
'{"hello":"world", "one": 1, "class": {"hello": "world"}}';
String nestedJson =
'{"foo": {"bar": "baz", "funny": {"how": "life", "seems": 2, "hate": "us sometimes"}}}';
var simple = god.deserialize(simpleJson ) as Map;
var nested = god.deserialize(nestedJson) as Map;
expect(simple['hello'], equals('world'));
expect(simple['one'], equals(1));
expect(simple['class']['hello'], equals('world'));
expect(nested['foo']['bar'], equals('baz'));
expect(nested['foo']['funny']['how'], equals('life'));
expect(nested['foo']['funny']['seems'], equals(2));
expect(nested['foo']['funny']['hate'], equals('us sometimes'));
}
class Pokedex {
Map<String, int> pokemon;
}
testDeserializationOfMapsWithReflection() {
var s = '{"pokemon": {"Bulbasaur": 1, "Deoxys": 382}}';
var pokedex = god.deserialize(s, outputType: Pokedex) as Pokedex;
expect(pokedex.pokemon, hasLength(2));
expect(pokedex.pokemon['Bulbasaur'], 1);
expect(pokedex.pokemon['Deoxys'], 382);
}
testDeserializationOfListsAsWellAsViaReflection() {
String json = '''[
{
"hello": "world",
"nested": []
},
{
"hello": "dolly",
"nested": [
{
"bar": "baz"
},
{
"bar": "fight"
}
]
}
]
''';
var list = god.deserialize(json, outputType: (<SampleClass>[]).runtimeType) as List<SampleClass>;
SampleClass first = list[0];
SampleClass second = list[1];
expect(list.length, equals(2));
expect(first.hello, equals("world"));
expect(first.nested.length, equals(0));
expect(second.hello, equals("dolly"));
expect(second.nested.length, equals(2));
SampleNestedClass firstNested = second.nested[0];
SampleNestedClass secondNested = second.nested[1];
expect(firstNested.bar, equals("baz"));
expect(secondNested.bar, equals("fight"));
}
testDeserializationWithSchemaValidation() async {
String babelRcJson =
'{"presets":["es2015","stage-0"],"plugins":["add-module-exports"]}';
var deserialized = god.deserialize(babelRcJson, outputType: BabelRc) as BabelRc;
print(deserialized.presets.runtimeType);
expect(deserialized.presets is List, equals(true));
expect(deserialized.presets.length, equals(2));
expect(deserialized.presets[0], equals('es2015'));
expect(deserialized.presets[1], equals('stage-0'));
expect(deserialized.plugins is List, equals(true));
expect(deserialized.plugins.length, equals(1));
expect(deserialized.plugins[0], equals('add-module-exports'));
}

View file

@ -0,0 +1,131 @@
import 'package:dart2_constant/convert.dart';
import 'package:json_god/json_god.dart' as god;
import 'package:test/test.dart';
import 'shared.dart';
main() {
god.logger.onRecord.listen(printRecord);
group('serialization', () {
test('serialize primitives', testSerializationOfPrimitives);
test('serialize dates', testSerializationOfDates);
test('serialize maps', testSerializationOfMaps);
test('serialize lists', testSerializationOfLists);
test('serialize via reflection', testSerializationViaReflection);
test('serialize with schema validation',
testSerializationWithSchemaValidation);
});
}
testSerializationOfPrimitives() {
expect(god.serialize(1), equals("1"));
expect(god.serialize(1.4), equals("1.4"));
expect(god.serialize("Hi!"), equals('"Hi!"'));
expect(god.serialize(true), equals("true"));
expect(god.serialize(null), equals("null"));
}
testSerializationOfDates() {
DateTime date = new DateTime.now();
String s = god.serialize({'date': date});
print(s);
var deserialized = json.decode(s);
expect(deserialized['date'], equals(date.toIso8601String()));
}
testSerializationOfMaps() {
var simple = json.decode(god.serialize(
{'hello': 'world', 'one': 1, 'class': new SampleClass('world')}));
var nested = json.decode(god.serialize({
'foo': {
'bar': 'baz',
'funny': {'how': 'life', 'seems': 2, 'hate': 'us sometimes'}
}
}));
expect(simple['hello'], equals('world'));
expect(simple['one'], equals(1));
expect(simple['class']['hello'], equals('world'));
expect(nested['foo']['bar'], equals('baz'));
expect(nested['foo']['funny']['how'], equals('life'));
expect(nested['foo']['funny']['seems'], equals(2));
expect(nested['foo']['funny']['hate'], equals('us sometimes'));
}
testSerializationOfLists() {
List pandorasBox = [
1,
"2",
{"num": 3, "four": new SampleClass('five')},
new SampleClass('six')..nested.add(new SampleNestedClass('seven'))
];
String s = god.serialize(pandorasBox);
print(s);
var deserialized = json.decode(s);
expect(deserialized is List, equals(true));
expect(deserialized.length, equals(4));
expect(deserialized[0], equals(1));
expect(deserialized[1], equals("2"));
expect(deserialized[2] is Map, equals(true));
expect(deserialized[2]['num'], equals(3));
expect(deserialized[2]['four'] is Map, equals(true));
expect(deserialized[2]['four']['hello'], equals('five'));
expect(deserialized[3] is Map, equals(true));
expect(deserialized[3]['hello'], equals('six'));
expect(deserialized[3]['nested'] is List, equals(true));
expect(deserialized[3]['nested'].length, equals(1));
expect(deserialized[3]['nested'][0] is Map, equals(true));
expect(deserialized[3]['nested'][0]['bar'], equals('seven'));
}
testSerializationViaReflection() {
SampleClass sample = new SampleClass('world');
for (int i = 0; i < 3; i++) {
sample.nested.add(new SampleNestedClass('baz'));
}
String s = god.serialize(sample);
print(s);
var deserialized = json.decode(s);
expect(deserialized['hello'], equals('world'));
expect(deserialized['nested'] is List, equals(true));
expect(deserialized['nested'].length == 3, equals(true));
expect(deserialized['nested'][0]['bar'], equals('baz'));
expect(deserialized['nested'][1]['bar'], equals('baz'));
expect(deserialized['nested'][2]['bar'], equals('baz'));
}
testSerializationWithSchemaValidation() async {
BabelRc babelRc = new BabelRc(
presets: ['es2015', 'stage-0'], plugins: ['add-module-exports']);
String s = god.serialize(babelRc);
print(s);
var deserialized = json.decode(s);
expect(deserialized['presets'] is List, equals(true));
expect(deserialized['presets'].length, equals(2));
expect(deserialized['presets'][0], equals('es2015'));
expect(deserialized['presets'][1], equals('stage-0'));
expect(deserialized['plugins'] is List, equals(true));
expect(deserialized['plugins'].length, equals(1));
expect(deserialized['plugins'][0], equals('add-module-exports'));
//Map babelRc2 = {'presets': 'Hello, world!'};
String json2 = god.serialize(babelRc);
print(json2);
}

View file

@ -0,0 +1,51 @@
import 'package:logging/logging.dart';
import 'package:json_god/json_god.dart';
import 'package:stack_trace/stack_trace.dart';
void printRecord(LogRecord rec) {
print(rec);
if (rec.error != null) print(rec.error);
if (rec.stackTrace != null) print(new Chain.forTrace(rec.stackTrace).terse);
}
class SampleNestedClass {
String bar;
SampleNestedClass([String this.bar]);
}
class SampleClass {
String hello;
List<SampleNestedClass> nested = [];
SampleClass([String this.hello]);
}
@WithSchemaUrl(
"http://raw.githubusercontent.com/SchemaStore/schemastore/master/src/schemas/json/babelrc.json")
class BabelRc {
List<String> presets;
List<String> plugins;
BabelRc(
{List<String> this.presets: const [],
List<String> this.plugins: const []});
}
@WithSchema(const {
r"$schema": "http://json-schema.org/draft-04/schema#",
"title": "Validated Sample Class",
"description": "Sample schema for validation via JSON God",
"type": "object",
"hello": const {"description": "A friendly greeting.", "type": "string"},
"nested": const {
"description": "A list of NestedSampleClass items within this instance.",
"type": "array",
"items": const {
"type": "object",
"bar": const {"description": "Filler text", "type": "string"}
}
},
"required": const ["hello", "nested"]
})
class ValidatedSampleClass {}

View file

@ -0,0 +1,32 @@
import 'package:json_god/json_god.dart' as god;
import 'package:test/test.dart';
import 'shared.dart';
main() {
god.logger.onRecord.listen(printRecord);
test('fromJson', () {
var foo = god.deserialize('{"bar":"baz"}', outputType: Foo) as Foo;
expect(foo is Foo, true);
expect(foo.text, equals('baz'));
});
test('toJson', () {
var foo = new Foo(text: 'baz');
var data = god.serializeObject(foo);
expect(data, equals({'bar': 'baz', 'foo': 'poobaz'}));
});
}
class Foo {
String text;
String get foo => 'poo$text';
Foo({this.text});
factory Foo.fromJson(Map json) => new Foo(text: json['bar'].toString());
Map toJson() => {'bar': text, 'foo': foo};
}