├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── dub.json
├── example
├── .gitignore
├── Makefile
├── dub.json
├── entities.json
└── src
│ ├── animator.d
│ ├── app.d
│ ├── component.d
│ ├── entity.d
│ ├── geometry.d
│ └── sprite.d
├── src
└── jsonizer
│ ├── common.d
│ ├── exceptions.d
│ ├── fromjson.d
│ ├── jsonize.d
│ ├── package.d
│ └── tojson.d
└── tests
├── classes.d
├── structs.d
└── types.d
/.gitignore:
--------------------------------------------------------------------------------
1 | # dub
2 | .dub/
3 | dub.selections.json
4 | __test__*
5 |
6 | # docs
7 | docs/
8 | docs.json
9 | __dummy.html
10 |
11 | # library
12 | *.a
13 | *.lib
14 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 rcorre
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | SETCOMPILER=
2 | ifdef DC
3 | SETCOMPILER="--compiler=$(DC)"
4 | endif
5 |
6 | all: debug
7 |
8 | debug:
9 | @dub build $(SETCOMPILER) --build=debug --quiet
10 |
11 | release:
12 | @dub build $(SETCOMPILER) release --quiet
13 |
14 | test:
15 | @dub test $(SETCOMPILER)
16 |
17 | docs:
18 | @dub build $(SETCOMPILER) --build=ddox --quiet
19 |
20 | clean:
21 | @dub clean
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | jsonizer: D language JSON serializer
2 | ===
3 |
4 | The primary purpose of **jsonizer** is to automate the generation of methods
5 | needed to serialize and deserialize user-defined D structs and classes from JSON
6 | data. jsonizer is not a standalone json parser, but rather a convenience layer
7 | on top of `std.json`, allowing you to more easily work with `JSONValue` objects.
8 |
9 | To use jsonizer, the main components you ened to be aware of are
10 | the methods `fromJSON!T` and `toJSON`, the attribute `@jsonize`, and the mixin
11 | template `JsonizeMe`.
12 |
13 | ## Overview
14 | Jsonizer consists of the following modules:
15 |
16 | - `jsonizer.fromjson`
17 | - parse a `T` from a `JSONValue` using `fromJSON!T`
18 | - parse a `T` from a json string using `fromJSONString!T`
19 | - parse a `T` from a json file using `readJSON!T`
20 | - `jsonizer.tojson`
21 | - convert a `T` to a `JSONValue` using `toJSON!T`
22 | - convert a `T` to a json string using `toJSONString!T`
23 | - write a `T` to a json file using `writeJSON!T`
24 | - `jsonizer.jsonize`
25 | - mixin `JsonizeMe` to enable json serialization for a user-defined type
26 | - use `@jsonize` to mark members for serialization
27 | - `jsonizer.all`
28 | - imports `jsonizer.tojson`, `jsonizer.fromjson`, and `jsonizer.jsonize`
29 |
30 | ## fromJSON!T
31 | `fromJSON!T` converts a `JSONValue` into an object of type `T`.
32 |
33 | ```d
34 | import jsonizer.fromjson;
35 | JSONValue json; // lets assume this has some data in it
36 | int i = json.fromJSON!int;
37 | MyEnum e = json.fromJSON!MyEnum;
38 | MyStruct[] s = json.fromJSON!(MyStruct[]);
39 | MyClass[string] c = json.fromJSON!(MyClass[string]);
40 | ```
41 |
42 | `fromJSON!T` will fail by throwing a `JsonizeTypeException` if the json object's type is not
43 | something it knows how to convert to `T`.
44 |
45 | For primitive types, `fromJSON` leans on the side of flexibility -- for example,
46 | `fromJSON!int` on a json entry of type `string` will try to parse an `int` from
47 | the `string`.
48 |
49 | For user-defined types, you have to do a little work to set up your struct or
50 | class for jsonizer.
51 |
52 | ## @jsonize and JsonizeMe
53 | The simplest way to make your type support json serialization is to mark its
54 | members with the `@jsonize` attribute and have `mixin JsonizeMe;` somewhere in
55 | your type definition. For example:
56 |
57 | ```d
58 | struct S {
59 | mixin JsonizeMe; // this is required to support jsonization
60 |
61 | @jsonize { // public serialized members
62 | int x;
63 | float f;
64 | }
65 | string dontJsonMe; // jsonizer won't touch members not marked with @jsonize
66 | }
67 | ```
68 |
69 | The above could be deserialized by calling `fromJSON!S` from a json object like:
70 |
71 | ```json
72 | { "x": 5, "f": 1.2 }
73 | ```
74 |
75 | Your struct could be converted back into a `JSONValue` by calling `toJSON` on an
76 | instance of it.
77 |
78 | jsonizer can do more than just convert public members though:
79 |
80 | ```d
81 | struct S {
82 | mixin JsonizeMe; // this is required to support jsonization
83 |
84 | // jsonize can convert private members too.
85 | // by default, jsonizer looks for a key in the json matching the member name
86 | // you can change this by passing a string to @jsonize
87 | private @jsonize("f") float _f;
88 |
89 | // you can use properties for more complex serialization
90 | // this is useful for converting types that are non-primitive
91 | // but also not defined by you, like std.datetime's Date
92 | private Date _date;
93 | @property @jsonize {
94 | string date() { return dateToString(_date); }
95 | void date(string str) { _date = dateFromString(str); }
96 | }
97 | }
98 | ```
99 |
100 | Assuming `dateToString` and `dateFromString` are some functions you defined, the
101 | above could be `fromJSON`ed from a json object looking like:
102 |
103 | ```json
104 | { "f": 2.1, "date": "2015-05-01" }
105 | ```
106 |
107 | The above examples work on both classes and structs provided the following:
108 |
109 | 1. Your type mixes in `JsonizeMe`
110 | 2. Your members are marked with `@jsonize`
111 | 3. Your type has a no-args constructor
112 |
113 | ### Optional members
114 | By default, if a matching json entry is not found for a member marked with `@jsonize`,
115 | deserialization will fail.
116 | If this is not desired for a given member, mark it with `JsonizeIn.opt`.
117 |
118 | ```d
119 | class MyClass {
120 | @jsonize int i;
121 | @jsonize(JsonizeIn.opt) float f;
122 | }
123 | ```
124 |
125 | In the above example `json.fromJSON!MyClass` will fail if it does not find a key named "i" in the
126 | json object, but will silently ignore the abscence of a key "f".
127 |
128 | Missing non-optional members trigger a `JsonizeMismatchException`, which contains a list of the
129 | missing keys in `missingKeys`:
130 |
131 | ```d
132 | auto ex = collectException!JsonizeMismatchException(`{ "q": 5.0 }`.parseJSON.fromJSON!MyClass);
133 |
134 | assert(ex.missingKeys == [ "i" ]);
135 | ```
136 |
137 | The way `@jsonize` takes parameters is rather flexible. While I can't condone making your class look
138 | like the below example, it demonstrates the flexibility of `@jsonize`:
139 |
140 | ```d
141 | class TotalMess {
142 | @jsonize(JsonizeIn.opt) {
143 | @jsonize("i") int _i;
144 | @jsonize("f", JsonizeIn.yes) float _f;
145 | @jsonize(JsonizeIn.yes, "s") float _s;
146 | }
147 | }
148 | ```
149 | As the above shows, parameters may be passed in any order to @jsonize.
150 |
151 | If you want to serialize only non-default (`val != typeof(val).init`) fields, you can use `JsonizeOut`
152 |
153 | ```d
154 | class TotalMess {
155 | @jsonize(JsonizeOut.opt) {
156 | @jsonize("i") int _i;
157 | @jsonize("f", JsonizeOut.yes) float _f;
158 | @jsonize(JsonizeOut.no, "s") float _s; // never serialized, only requred for deserialization
159 | }
160 | }
161 | ```
162 |
163 | As a shortcut to `JsonizeIn/JsonizeOut`, you can just use `Jsonize`:
164 |
165 | ```d
166 | class TotalMess {
167 | // equivalent to: @jsonize(JsonizeIn.opt, JsonizeOut.opt)
168 | @jsonize(Jsonize.opt) int a;
169 | }
170 | ```
171 |
172 |
173 | ### Extra Members
174 | If you would like to ensure that every entry in a json object is being
175 | deserialized, you can pass `JsonizeIgnoreExtraKeys.no` to `JsonizeMe`.
176 | In the example below, `fromJSON!S(jobject)` will throw a
177 | `JsonizeMismatchException` than if fields other than `s` and `i` exist in
178 | `jobject`.
179 |
180 | ```d
181 | struct S {
182 | mixin JsonizeMe(JsonizeIgnoreExtraKeys.no);
183 | string s;
184 | int i;
185 | }
186 | ```
187 |
188 | When a `JsonizeMismatchException` is caught, you can inspect the extra fields by
189 | looking at `extraKeys`:
190 |
191 | ```d
192 | auto ex = collectException!JsonizeMismatchException(
193 | `{ "i": 5, "f": 0.2, "s": "hi"}`.parseJSON.fromJSON!S);
194 |
195 | assert(ex.extraKeys == [ "f" ]);
196 | ```
197 |
198 | ## Constructors
199 | In some cases, #3 above may not seem so great. What if your type needs to
200 | support serialization but shouldn't have a default constructor?
201 | In this case, you want to `@jsonize` your constructor:
202 |
203 | ```d
204 | class Custom {
205 | mixin JsonizeMe;
206 |
207 | @jsonize this(int i, string s = "hello") {
208 | _i = i;
209 | _s = s;
210 | }
211 |
212 | private:
213 | @jsonize("i") int _i;
214 | @jsonize("s") string _s;
215 | }
216 | ```
217 |
218 | Given a type `T` with one or more constructors tagged with `@jsonize`,
219 | `fromJSON!T` will try to match the member names and types to a constructor and
220 | invoke that with the corresponding values from the json object.
221 | Parameters with default values are considered optional; if they are not found in
222 | the json, the default value will be used. The above example could be constructed
223 | from json looking like:
224 |
225 | ```json
226 | { "i": 5, "s": "hi" }
227 | ```
228 |
229 | If "s" were not present, it would be assigned the value "hello".
230 |
231 | Note that while you can `@jsonize` multiple constructors, there should be no
232 | overlap between situations that could satisfy them. If a given json object could
233 | possibly match multiple constructors, jsonizer chooses arbitrarily (it does not
234 | attempt to pick the 'most appropriate' constructor).
235 |
236 | The method of jsonizing your constructor is also useful for types that need to
237 | perform a more complex setup sequence.
238 |
239 | Also note that when using `@jsonize` constructors, mixing in `JsonizeMe` and
240 | marking members with `@jsonize` are only necessary for serialization -- if your
241 | object only needs to support deserialization, marking a constructor is
242 | sufficient.
243 |
244 | If a type has no default (no-args) constructor and jsonizer cannot invoke any constructor marked
245 | with @jsonize, it will throw a `JsonizeConstructorException` which provides info on what
246 | constructors were attempted.
247 |
248 | ### Primitive Constructors
249 | If a type has a constructor marked with @jsonize that takes a single argument,
250 | it can be constructed from a JSONValue of non-object type. For example, the
251 | following struct could be constructed from a json integer:
252 |
253 | ```d
254 | class IntStruct {
255 | mixin JsonizeMe;
256 | int i;
257 | @jsonize this(int i) { this.i = i; }
258 | }
259 | ```
260 |
261 | ## Factory construction
262 | This is one of the newer and least tested features of jsonizer.
263 | Suppose you have the following classes:
264 |
265 | ```d
266 | module test;
267 | class TestComponent {
268 | mixin JsonizeMe;
269 | @jsonize int c;
270 | }
271 |
272 | class TestCompA : TestComponent {
273 | mixin JsonizeMe;
274 | @jsonize int a;
275 | }
276 |
277 | class TestCompB : TestComponent {
278 | mixin JsonizeMe;
279 | @jsonize string b;
280 | }
281 | ```
282 |
283 | and the following json:
284 |
285 | ```json
286 | [
287 | {
288 | "class": "test.TestCompA",
289 | "c": 1,
290 | "a": 5
291 | },
292 | {
293 | "class": "test.TestCompB",
294 | "c": 2,
295 | "b": "hello"
296 | }
297 | ]
298 | ```
299 |
300 | Calling `fromJSON!(TestComponent[])` on a `JSONValue` parsed from the above json
301 | string should yield a TestComponent[] of length 2.
302 | While both have the static type `TestComponent`, one is actually a `TestCompA`
303 | and the other is a `TestCompB`, both with their fields appropriately populated.
304 |
305 | Behind the scenes, jsonizer looks for a special key 'class' in the json (chosen
306 | because class is a D keyword and could not be a member of your type). If it
307 | finds this, it calls Object.factory using the specified string. It then calls
308 | `populateFromJSON`, which is a method generated by the `JsonizeMe` mixin.
309 |
310 | For this to work, your type must:
311 | 1. Have a default constructor
312 | 2. mixin JsonizeMe in **every** class in the hierarchy
313 |
--------------------------------------------------------------------------------
/dub.json:
--------------------------------------------------------------------------------
1 | {
2 | "name" : "jsonizer",
3 | "description" : "Flexible JSON serializer requiring minimal boilerplate",
4 | "copyright" : "Copyright © 2015, rcorre",
5 | "authors" : ["rcorre"],
6 | "license" : "MIT",
7 | "targetType" : "library",
8 | "dependencies" : {
9 | },
10 | "configurations": [
11 | {
12 | "name": "library"
13 | },
14 | {
15 | "name": "unittest",
16 | "importPaths": [ "src", "tests" ],
17 | "sourcePaths": [ "src", "tests" ]
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | .dub
2 | docs.json
3 | __dummy.html
4 | example
5 | *.o
6 | *.obj
7 |
--------------------------------------------------------------------------------
/example/Makefile:
--------------------------------------------------------------------------------
1 | SETCOMPILER=
2 | ifdef DC
3 | SETCOMPILER="--compiler=$(DC)"
4 | endif
5 |
6 | all: debug
7 |
8 | debug:
9 | @dub build $(SETCOMPILER) --build=debug --quiet
10 |
11 | run:
12 | @dub run $(SETCOMPILER) --build=debug --quiet
13 |
14 | release:
15 | @dub build $(SETCOMPILER) release --quiet
16 |
17 | test:
18 | @dub test $(SETCOMPILER) --quiet
19 |
20 | clean:
21 | @dub clean
22 |
--------------------------------------------------------------------------------
/example/dub.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "description": "Example of using jsonizer in an Entity-Component framework",
4 | "copyright": "Copyright © 2015, erethar",
5 | "authors": ["erethar"],
6 | "dependencies": {
7 | "jsonizer": { "path": ".." }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/example/entities.json:
--------------------------------------------------------------------------------
1 | {
2 | "player": {
3 | "position": { "x": 5, "y": 40 },
4 | "components": [
5 | {
6 | "class": "animator.Animator",
7 | "frameTime": 0.033,
8 | "repeat": "loop"
9 | },
10 | {
11 | "class": "sprite.Sprite",
12 | "textureName": "person",
13 | "depth": 1,
14 | "textureRegion": {
15 | "x": 0,
16 | "y": 0,
17 | "w": 32,
18 | "h": 32
19 | }
20 | }
21 | ]
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/example/src/animator.d:
--------------------------------------------------------------------------------
1 | module animator;
2 |
3 | import std.string;
4 | import geometry;
5 | import component;
6 | import jsonizer.jsonize : jsonize, JsonizeMe;
7 |
8 | class Animator : Component {
9 | mixin JsonizeMe;
10 |
11 | enum Repeat {
12 | no,
13 | loop,
14 | reverse
15 | }
16 |
17 | @jsonize {
18 | float frameTime;
19 | Repeat repeat;
20 | }
21 |
22 | override string stringify() {
23 | enum fmt =
24 | `Animator Component:
25 | frameTime : %f
26 | repeat : %s`;
27 | return fmt.format(frameTime, repeat);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/example/src/app.d:
--------------------------------------------------------------------------------
1 | import std.stdio;
2 | import geometry;
3 | import entity;
4 | import component;
5 | import sprite;
6 | import animator;
7 | import jsonizer.fromjson : readJSON;
8 |
9 | private enum fileName = "entities.json";
10 |
11 | void main() {
12 | auto entities = fileName.readJSON!(Entity[string]);
13 | auto player = entities["player"];
14 | writefln("Player at position <%d,%d>", player.position.x, player.position.y);
15 | foreach(component ; player.components) {
16 | writeln(component.stringify);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/example/src/component.d:
--------------------------------------------------------------------------------
1 | module component;
2 |
3 | import jsonizer.jsonize : JsonizeMe;
4 |
5 | abstract class Component {
6 | mixin JsonizeMe;
7 |
8 | // create a string representation of this component, just to show it was populated
9 | string stringify();
10 | }
11 |
--------------------------------------------------------------------------------
/example/src/entity.d:
--------------------------------------------------------------------------------
1 | module entity;
2 |
3 | import geometry;
4 | import component;
5 | import jsonizer.jsonize : jsonize, JsonizeMe;
6 |
7 | class Entity {
8 | mixin JsonizeMe;
9 |
10 | @jsonize {
11 | Vector position;
12 | Component[] components;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/example/src/geometry.d:
--------------------------------------------------------------------------------
1 | module geometry;
2 |
3 | // try importing the entire jsonizer package
4 | import jsonizer;
5 |
6 | struct Vector {
7 | mixin JsonizeMe;
8 | @jsonize {
9 | int x, y;
10 | }
11 | }
12 |
13 | struct Rect {
14 | mixin JsonizeMe;
15 | @jsonize {
16 | int x, y, w, h;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/example/src/sprite.d:
--------------------------------------------------------------------------------
1 | module sprite;
2 |
3 | import std.string;
4 | import geometry;
5 | import component;
6 | import jsonizer.jsonize;
7 |
8 | class Sprite : Component {
9 | mixin JsonizeMe;
10 |
11 | @jsonize {
12 | string textureName;
13 | int depth;
14 | Rect textureRegion;
15 | }
16 |
17 | override string stringify() {
18 | enum fmt =
19 | `Sprite Component:
20 | textureName : %s
21 | textureRegion : [%d, %d, %d, %d]
22 | depth : %d`;
23 | return fmt.format(
24 | textureName,
25 | textureRegion.x, textureRegion.y, textureRegion.w, textureRegion.h,
26 | depth);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/jsonizer/common.d:
--------------------------------------------------------------------------------
1 | module jsonizer.common;
2 |
3 | /// use @jsonize to mark members to be (de)serialized from/to json
4 | /// use @jsonize to mark a single contructor to use when creating an object using extract
5 | /// use @jsonize("name") to make a member use the json key "name"
6 | /// use @jsonize(Jsonize.[yes/opt]) to choose whether the parameter is optional
7 | /// use @jsonize(JsonizeIn.[yes/opt/no]) to choose whether the parameter is optional for deserialization
8 | /// use @jsonize(JsonizeOut.[yes/opt/no]) to choose whether the parameter is optional for serialization
9 | struct jsonize {
10 | /// alternate name used to identify member in json
11 | string key;
12 |
13 | /// whether member is required during deserialization
14 | JsonizeIn perform_in = JsonizeIn.unspecified;
15 | /// whether serialized member
16 | JsonizeOut perform_out = JsonizeOut.unspecified;
17 |
18 | /// parameters to @jsonize may be specified in any order
19 | /// valid uses of @jsonize include:
20 | /// @jsonize
21 | /// @jsonize("foo")
22 | /// @jsonize(Jsonize.opt)
23 | /// @jsonize("bar", Jsonize.opt)
24 | /// @jsonize(Jsonize.opt, "bar")
25 | this(T ...)(T params) {
26 | foreach(idx , param ; params) {
27 | alias type = T[idx];
28 | static if (is(type == Jsonize)) {
29 | perform_in = cast(JsonizeIn)param;
30 | perform_out = cast(JsonizeOut)param;
31 | }
32 | else static if (is(type == JsonizeIn)) {
33 | perform_in = param;
34 | }
35 | else static if (is(type == JsonizeOut)) {
36 | perform_out = param;
37 | }
38 | else static if (is(type : string)) {
39 | key = param;
40 | }
41 | else {
42 | assert(0, "invalid @jsonize parameter of type " ~ typeid(type));
43 | }
44 | }
45 | }
46 | }
47 |
48 | /// Control the strictness with which a field is deserialized
49 | enum JsonizeIn
50 | {
51 | /// The default. Equivalent to `yes` unless overridden by another UDA.
52 | unspecified = 0,
53 | /// always deserialize this field, fail if it is not present
54 | yes = 1,
55 | /// deserialize if found, but continue without error if it is missing
56 | opt = 2,
57 | /// never deserialize this field
58 | no = 3
59 | }
60 |
61 | /// Control the strictness with which a field is serialized
62 | enum JsonizeOut
63 | {
64 | /// the default value -- equivalent to `yes`
65 | unspecified = 0,
66 | /// always serialize this field
67 | yes = 1,
68 | /// serialize only if it not equal to the initial value of the type
69 | opt = 2,
70 | /// never serialize this field
71 | no = 3
72 | }
73 |
74 | /// Shortcut for setting both `JsonizeIn` and `JsonizeOut`
75 | enum Jsonize
76 | {
77 | /// equivalent to JsonizeIn.yes, JsonizeOut.yes
78 | yes = 1,
79 | /// equivalent to JsonizeIn.opt, JsonizeOut.opt
80 | opt = 2
81 | }
82 |
83 | /// Use of `Jsonize(In,Out)`:
84 | unittest {
85 | import std.json : parseJSON;
86 | import std.exception : collectException, assertNotThrown;
87 | import jsonizer.jsonize : JsonizeMe;
88 | import jsonizer.fromjson : fromJSON;
89 | import jsonizer.exceptions : JsonizeMismatchException;
90 | static struct S {
91 | mixin JsonizeMe;
92 |
93 | @jsonize {
94 | int i; // i is non-opt (default)
95 | @jsonize(Jsonize.opt) {
96 | @jsonize("_s") string s; // s is optional
97 | @jsonize(Jsonize.yes) float f; // f is non-optional (overrides outer attribute)
98 | }
99 | }
100 | }
101 |
102 | assertNotThrown(`{ "i": 5, "f": 0.2}`.parseJSON.fromJSON!S);
103 | auto ex = collectException!JsonizeMismatchException(`{ "i": 5 }`.parseJSON.fromJSON!S);
104 |
105 | assert(ex !is null, "missing non-optional field 'f' should trigger JsonizeMismatchException");
106 | assert(ex.targetType == typeid(S));
107 | assert(ex.missingKeys == [ "f" ]);
108 | assert(ex.extraKeys == [ ]);
109 | }
110 |
111 | /// Whether to silently ignore json keys that do not map to serialized members.
112 | enum JsonizeIgnoreExtraKeys {
113 | no, /// silently ignore extra keys in the json object being deserialized
114 | yes /// fail if the json object contains a keys that does not map to a serialized field
115 | }
116 |
117 | /// Use of `JsonizeIgnoreExtraKeys`:
118 | unittest {
119 | import std.json : parseJSON;
120 | import std.exception : collectException, assertNotThrown;
121 | import jsonizer.jsonize : JsonizeMe;
122 | import jsonizer.fromjson : fromJSON;
123 | import jsonizer.exceptions : JsonizeMismatchException;
124 |
125 | static struct NoCares {
126 | mixin JsonizeMe;
127 | @jsonize {
128 | int i;
129 | float f;
130 | }
131 | }
132 |
133 | static struct VeryStrict {
134 | mixin JsonizeMe!(JsonizeIgnoreExtraKeys.no);
135 | @jsonize {
136 | int i;
137 | float f;
138 | }
139 | }
140 |
141 | // no extra fields, neither should throw
142 | assertNotThrown(`{ "i": 5, "f": 0.2}`.parseJSON.fromJSON!NoCares);
143 | assertNotThrown(`{ "i": 5, "f": 0.2}`.parseJSON.fromJSON!VeryStrict);
144 |
145 | // extra field "s"
146 | // `NoCares` ignores extra keys, so it will not throw
147 | assertNotThrown(`{ "i": 5, "f": 0.2, "s": "hi"}`.parseJSON.fromJSON!NoCares);
148 | // `VeryStrict` does not ignore extra keys
149 | auto ex = collectException!JsonizeMismatchException(
150 | `{ "i": 5, "f": 0.2, "s": "hi"}`.parseJSON.fromJSON!VeryStrict);
151 |
152 | assert(ex !is null, "extra field 's' should trigger JsonizeMismatchException");
153 | assert(ex.targetType == typeid(VeryStrict));
154 | assert(ex.missingKeys == [ ]);
155 | assert(ex.extraKeys == [ "s" ]);
156 | }
157 |
158 | /// Customize the behavior of `toJSON` and `fromJSON`.
159 | struct JsonizeOptions {
160 | /**
161 | * A default-constructed `JsonizeOptions`.
162 | * Used implicilty if no explicit options are given to `fromJSON` or `toJSON`.
163 | */
164 | static immutable defaults = JsonizeOptions.init;
165 |
166 | /**
167 | * The key of a field identifying the D type of a json object.
168 | *
169 | * If this key is found in the json object, `fromJSON` will try to factory
170 | * construct an object of the type identified.
171 | *
172 | * This is useful when deserializing a collection of some type `T`, where the
173 | * actual instances may be different subtypes of `T`.
174 | *
175 | * Setting `classKey` to null will disable factory construction.
176 | */
177 | string classKey = "class";
178 |
179 | /**
180 | * A function to attempt identifier remapping from the name found under `classKey`.
181 | *
182 | * If this function is provided, then when the `classKey` is found, this function
183 | * will attempt to remap the value. This function should return either the fully
184 | * qualified class name or null. Returned non-null values indicate that the
185 | * remapping has succeeded. A null value will indicate the mapping has failed
186 | * and the original value will be used in the object factory.
187 | *
188 | * This is particularly useful when input JSON has not originated from D.
189 | */
190 | string delegate(string) classMap;
191 | }
192 |
193 | package:
194 | // Get the json key corresponding to `T.member`.
195 | template jsonKey(T, string member) {
196 | alias attrs = T._getUDAs!(member, jsonize);
197 | static if (!attrs.length)
198 | enum jsonKey = member;
199 | else static if (attrs[$ - 1].key)
200 | enum jsonKey = attrs[$ - 1].key;
201 | else
202 | enum jsonKey = member;
203 | }
204 |
--------------------------------------------------------------------------------
/src/jsonizer/exceptions.d:
--------------------------------------------------------------------------------
1 | /**
2 | * Defines the exceptions that Jsonizer may throw.
3 | *
4 | * Authors: rcorre
5 | * License: MIT
6 | * Copyright: Copyright © 2015, rcorre
7 | * Date: 3/24/15
8 | */
9 | module jsonizer.exceptions;
10 |
11 | import std.json : JSONValue, JSONType;
12 | import std.string : format, join;
13 | import std.traits : ParameterTypeTuple, ParameterIdentifierTuple;
14 | import std.meta : aliasSeqOf;
15 | import std.range : iota;
16 | import std.typetuple : staticMap;
17 |
18 | /// Base class of any exception thrown by `jsonizer`.
19 | class JsonizeException : Exception {
20 | this(string msg) {
21 | super(msg);
22 | }
23 | }
24 |
25 | /// Thrown when `fromJSON` cannot convert a `JSONValue` into the requested type.
26 | class JsonizeTypeException : Exception {
27 | private enum fmt =
28 | "fromJSON!%s expected json type to be one of %s but got json type %s. json input: %s";
29 |
30 | const {
31 | TypeInfo targetType; /// Type jsonizer was attempting to deserialize to.
32 | JSONValue json; /// The json value that was being deserialized
33 | JSONType[] expected; /// The JSON_TYPEs that would have been acceptable
34 | }
35 |
36 | this(TypeInfo targetType, JSONValue json, JSONType[] expected ...) {
37 | super(fmt.format(targetType, expected, json.type, json));
38 |
39 | this.targetType = targetType;
40 | this.json = json;
41 | this.expected = expected;
42 | }
43 | }
44 |
45 | unittest {
46 | import std.algorithm : canFind;
47 |
48 | auto json = JSONValue(4.2f);
49 | auto targetType = typeid(bool);
50 | auto expected = [JSONType.true_, JSONType.false_];
51 |
52 | auto e = new JsonizeTypeException(targetType, json, JSONType.true_, JSONType.false_);
53 |
54 | assert(e.json == json);
55 | assert(e.targetType == targetType);
56 | assert(e.expected == expected);
57 | assert(e.msg.canFind("fromJSON!bool"),
58 | "JsonizeTypeException should report type argument");
59 | assert(e.msg.canFind("true_") && e.msg.canFind("false_"),
60 | "JsonizeTypeException should report all acceptable json types");
61 | }
62 |
63 | /// Thrown when the keys of a json object fail to match up with the members of the target type.
64 | class JsonizeMismatchException : JsonizeException {
65 | private enum fmt =
66 | "Failed to deserialize %s.\n" ~
67 | "Missing non-optional members: %s.\n" ~
68 | "Extra keys in json: %s.\n";
69 |
70 | const {
71 | TypeInfo targetType; /// Type jsonizer was attempting to deserialize to.
72 | string[] extraKeys; /// keys present in json that do not match up to a member.
73 | string[] missingKeys; /// non-optional members that were not found in the json.
74 | }
75 |
76 | this(TypeInfo targetType, string[] extraKeys, string[] missingKeys) {
77 | super(fmt.format(targetType, missingKeys, extraKeys));
78 |
79 | this.targetType = targetType;
80 | this.extraKeys = extraKeys;
81 | this.missingKeys = missingKeys;
82 | }
83 | }
84 |
85 | unittest {
86 | import std.algorithm : all, canFind;
87 | import std.conv : to;
88 |
89 | static class MyClass { }
90 |
91 | auto targetType = typeid(MyClass);
92 | auto extraKeys = [ "who", "what" ];
93 | auto missingKeys = [ "where" ];
94 |
95 | auto e = new JsonizeMismatchException(targetType, extraKeys, missingKeys);
96 | assert(e.targetType == targetType);
97 | assert(e.extraKeys == extraKeys);
98 | assert(e.missingKeys == missingKeys);
99 | assert(e.msg.canFind(targetType.to!string),
100 | "JsonizeMismatchException should report type argument");
101 | assert(extraKeys.all!(x => e.msg.canFind(x)),
102 | "JsonizeTypeException should report all extra keys");
103 | assert(missingKeys.all!(x => e.msg.canFind(x)),
104 | "JsonizeTypeException should report all missing keys");
105 | }
106 |
107 | /// Thrown when a type has no default constructor and the custom constructor cannot be fulfilled.
108 | class JsonizeConstructorException : JsonizeException {
109 | private enum fmt =
110 | "%s has no default constructor, and none of the following constructors could be fulfilled: \n" ~
111 | "%s\n" ~
112 | "json object:\n %s";
113 |
114 | const {
115 | TypeInfo targetType; /// Tye type jsonizer was attempting to deserialize to.
116 | JSONValue json; /// The json value that was being deserialized
117 | }
118 |
119 | /// Construct and throw a `JsonizeConstructorException`
120 | /// Params:
121 | /// T = Type being deserialized
122 | /// Ctors = constructors that were attempted
123 | /// json = json object being deserialized
124 | static void doThrow(T, Ctors ...)(JSONValue json) {
125 | static if (Ctors.length > 0) {
126 | auto signatures = [staticMap!(ctorSignature, Ctors)].join("\n");
127 | }
128 | else {
129 | auto signatures = "";
130 | }
131 |
132 | throw new JsonizeConstructorException(typeid(T), signatures, json);
133 | }
134 |
135 | private this(TypeInfo targetType, string ctorSignatures, JSONValue json) {
136 | super(fmt.format(targetType, ctorSignatures, json));
137 |
138 | this.targetType = targetType;
139 | this.json = json;
140 | }
141 | }
142 |
143 | private:
144 | // Represent the function signature of a constructor as a string.
145 | template ctorSignature(alias ctor) {
146 | alias params = ParameterIdentifierTuple!ctor;
147 | alias types = ParameterTypeTuple!ctor;
148 |
149 | // build a string "type1 param1, type2 param2, ..., typeN paramN"
150 | static string paramString() {
151 | string s = "";
152 |
153 | foreach(i ; aliasSeqOf!(params.length.iota)) {
154 | s ~= types[i].stringof ~ " " ~ params[i];
155 |
156 | static if (i < params.length - 1) {
157 | s ~= ", ";
158 | }
159 | }
160 |
161 | return s;
162 | }
163 |
164 | enum ctorSignature = "this(%s)".format(paramString);
165 | }
166 |
167 | unittest {
168 | static class Foo {
169 | this(string s, int i, float f) { }
170 | }
171 |
172 | assert(ctorSignature!(Foo.__ctor) == "this(string s, int i, float f)");
173 | }
174 |
--------------------------------------------------------------------------------
/src/jsonizer/fromjson.d:
--------------------------------------------------------------------------------
1 | /**
2 | * Contains functions for deserializing JSON data.
3 | *
4 | * Authors: rcorre
5 | * License: MIT
6 | * Copyright: Copyright © 2015, rcorre
7 | * Date: 3/23/15
8 | */
9 | module jsonizer.fromjson;
10 |
11 | import std.json;
12 | import std.conv;
13 | import std.file;
14 | import std.meta;
15 | import std.range;
16 | import std.traits;
17 | import std.string;
18 | import std.algorithm;
19 | import std.exception;
20 | import std.typecons;
21 | import jsonizer.exceptions;
22 | import jsonizer.common;
23 |
24 | // HACK: this is a hack to allow referencing this particular overload using
25 | // &fromJSON!T in the JsonizeMe mixin
26 | T _fromJSON(T)(JSONValue json,
27 | in ref JsonizeOptions options = JsonizeOptions.defaults)
28 | {
29 | return fromJSON!T(json, options);
30 | }
31 |
32 | /**
33 | * Deserialize json into a value of type `T`.
34 | *
35 | * Params:
36 | * T = Target type. can be any primitive/builtin D type, or any
37 | * user-defined type using the `JsonizeMe` mixin.
38 | * json = `JSONValue` to deserialize.
39 | * options = configures the deserialization behavior.
40 | */
41 | T fromJSON(T)(JSONValue json,
42 | in ref JsonizeOptions options = JsonizeOptions.defaults)
43 | {
44 | // JSONValue -- identity
45 | static if (is(T == JSONValue))
46 | return json;
47 |
48 | // enumeration
49 | else static if (is(T == enum)) {
50 | enforceJsonType!T(json, JSONType.string);
51 | return to!T(json.str);
52 | }
53 |
54 | // boolean
55 | else static if (is(T : bool)) {
56 | if (json.type == JSONType.true_)
57 | return true;
58 | else if (json.type == JSONType.false_)
59 | return false;
60 |
61 | // expected 'true' or 'false'
62 | throw new JsonizeTypeException(typeid(bool), json, JSONType.true_, JSONType.false_);
63 | }
64 |
65 | // string
66 | else static if (is(T : string)) {
67 | if (json.type == JSONType.null_) { return null; }
68 | enforceJsonType!T(json, JSONType.string);
69 | return cast(T) json.str;
70 | }
71 |
72 | // numeric
73 | else static if (is(T : real)) {
74 | switch(json.type) {
75 | case JSONType.float_:
76 | return cast(T) json.floating;
77 | case JSONType.integer:
78 | return cast(T) json.integer;
79 | case JSONType.uinteger:
80 | return cast(T) json.uinteger;
81 | case JSONType.string:
82 | enforce(json.str.isNumeric, format("tried to extract %s from json string %s", T.stringof, json.str));
83 | return to!T(json.str); // try to parse string as int
84 | default:
85 | }
86 |
87 | throw new JsonizeTypeException(typeid(bool), json,
88 | JSONType.float_, JSONType.integer, JSONType.uinteger, JSONType.string);
89 | }
90 |
91 | // array
92 | else static if (isArray!T) {
93 | if (json.type == JSONType.null_) { return T.init; }
94 | enforceJsonType!T(json, JSONType.array);
95 | alias ElementType = ForeachType!T;
96 | T vals;
97 | foreach(idx, val ; json.array) {
98 | static if (isStaticArray!T) {
99 | vals[idx] = val.fromJSON!ElementType(options);
100 | }
101 | else {
102 | vals ~= val.fromJSON!ElementType(options);
103 | }
104 | }
105 | return vals;
106 | }
107 |
108 | // associative array
109 | else static if (isAssociativeArray!T) {
110 | static assert(is(KeyType!T : string), "toJSON requires string keys for associative array");
111 | if (json.type == JSONType.null_) { return null; }
112 | enforceJsonType!T(json, JSONType.object);
113 | alias ValType = ValueType!T;
114 | T map;
115 | foreach(key, val ; json.object) {
116 | map[key] = fromJSON!ValType(val, options);
117 | }
118 | return map;
119 | }
120 |
121 | // user-defined class or struct
122 | else static if (!isBuiltinType!T) {
123 | return fromJSONImpl!T(json, null, options);
124 | }
125 |
126 | // by the time we get here, we've tried pretty much everything
127 | else {
128 | static assert(0, "fromJSON can't handle a " ~ T.stringof);
129 | }
130 | }
131 |
132 | /// Extract booleans from json values.
133 | unittest {
134 | assert(JSONValue(false).fromJSON!bool == false);
135 | assert(JSONValue(true).fromJSON!bool == true);
136 | }
137 |
138 | /// Extract a string from a json string.
139 | unittest {
140 | assert(JSONValue("asdf").fromJSON!string == "asdf");
141 | }
142 |
143 | /// Extract various numeric types.
144 | unittest {
145 | assert(JSONValue(1).fromJSON!int == 1);
146 | assert(JSONValue(2u).fromJSON!uint == 2u);
147 | assert(JSONValue(3.0).fromJSON!double == 3.0);
148 |
149 | // fromJSON accepts numeric strings when a numeric conversion is requested
150 | assert(JSONValue("4").fromJSON!long == 4L);
151 | }
152 |
153 | /// Convert a json string into an enum value.
154 | unittest {
155 | enum Category { one, two }
156 | assert(JSONValue("one").fromJSON!Category == Category.one);
157 | }
158 |
159 | /// Convert a json array into an array.
160 | unittest {
161 | auto a = [ 1, 2, 3 ];
162 | assert(JSONValue(a).fromJSON!(int[]) == a);
163 | }
164 |
165 | /// Convert a json object to an associative array.
166 | unittest {
167 | auto aa = ["a": 1, "b": 2];
168 | assert(JSONValue(aa).fromJSON!(int[string]) == aa);
169 | }
170 |
171 | /// Convert a json object to a user-defined type.
172 | /// See the docs for `JsonizeMe` for more detailed examples.
173 | unittest {
174 | import jsonizer.jsonize;
175 | static struct MyStruct {
176 | mixin JsonizeMe;
177 |
178 | @jsonize int i;
179 | @jsonize string s;
180 | float f;
181 | }
182 |
183 | auto json = `{ "i": 5, "s": "tally-ho!" }`.parseJSON;
184 | auto val = json.fromJSON!MyStruct;
185 | assert(val.i == 5);
186 | assert(val.s == "tally-ho!");
187 | }
188 |
189 | /**
190 | * Extract a value from a json object by its key.
191 | *
192 | * Throws if `json` is not of `JSONType.object` or the key does not exist.
193 | *
194 | * Params:
195 | * T = Target type. can be any primitive/builtin D type, or any
196 | * user-defined type using the `JsonizeMe` mixin.
197 | * json = `JSONValue` to deserialize.
198 | * key = key of desired value within the object.
199 | * options = configures the deserialization behavior.
200 | */
201 | T fromJSON(T)(JSONValue json,
202 | string key,
203 | in ref JsonizeOptions options = JsonizeOptions.defaults)
204 | {
205 | enforceJsonType!T(json, JSONType.object);
206 | enforce(key in json.object, "tried to extract non-existent key " ~ key ~ " from JSONValue");
207 | return fromJSON!T(json.object[key], options);
208 | }
209 |
210 | /// Directly extract values from an object by their keys.
211 | unittest {
212 | auto aa = ["a": 1, "b": 2];
213 | auto json = JSONValue(aa);
214 | assert(json.fromJSON!int("a") == 1);
215 | assert(json.fromJSON!ulong("b") == 2L);
216 | }
217 |
218 | /// Deserialize an instance of a user-defined type from a json object.
219 | unittest {
220 | import jsonizer.jsonize;
221 |
222 | static struct Foo {
223 | mixin JsonizeMe;
224 | @jsonize int i;
225 | @jsonize string[] a;
226 | }
227 |
228 | auto foo = `{"i": 12, "a": [ "a", "b" ]}`.parseJSON.fromJSON!Foo;
229 | assert(foo == Foo(12, [ "a", "b" ]));
230 | }
231 |
232 | /**
233 | * Extract a value from a json object by its key.
234 | *
235 | * Throws if `json` is not of `JSONType.object`.
236 | * Return `defaultVal` if the key does not exist.
237 | *
238 | * Params:
239 | * T = Target type. can be any primitive/builtin D type, or any
240 | * user-defined type using the `JsonizeMe` mixin.
241 | * json = `JSONValue` to deserialize.
242 | * key = key of desired value within the object.
243 | * defaultVal = value to return if key is not found
244 | * options = configures the deserialization behavior.
245 | */
246 | T fromJSON(T)(JSONValue json,
247 | string key,
248 | T defaultVal,
249 | in ref JsonizeOptions options = JsonizeOptions.defaults)
250 | {
251 | enforceJsonType!T(json, JSONType.object);
252 | return (key in json.object) ? fromJSON!T(json.object[key]) : defaultVal;
253 | }
254 |
255 | /// Substitute default values when keys aren't present.
256 | unittest {
257 | auto aa = ["a": 1, "b": 2];
258 | auto json = JSONValue(aa);
259 | assert(json.fromJSON!int("a", 7) == 1);
260 | assert(json.fromJSON!int("c", 7) == 7);
261 | }
262 |
263 | /*
264 | * Convert a json value in its string representation into a type `T`
265 | * Params:
266 | * T = Target type. can be any primitive/builtin D type, or any
267 | * user-defined type using the `JsonizeMe` mixin.
268 | * json = JSON-formatted string to deserialize.
269 | * options = configures the deserialization behavior.
270 | */
271 | T fromJSONString(T)(string json,
272 | in ref JsonizeOptions options = JsonizeOptions.defaults)
273 | {
274 | return fromJSON!T(json.parseJSON, options);
275 | }
276 |
277 | /// Use `fromJSONString` to parse from a json `string` rather than a `JSONValue`
278 | unittest {
279 | assert(fromJSONString!(int[])("[1, 2, 3]") == [1, 2, 3]);
280 | }
281 |
282 | /**
283 | * Read a json-constructable object from a file.
284 | * Params:
285 | * T = Target type. can be any primitive/builtin D type, or any
286 | * user-defined type using the `JsonizeMe` mixin.
287 | * path = filesystem path to json file
288 | * options = configures the deserialization behavior.
289 | */
290 | T readJSON(T)(string path,
291 | in ref JsonizeOptions options = JsonizeOptions.defaults)
292 | {
293 | auto json = parseJSON(readText(path));
294 | return fromJSON!T(json, options);
295 | }
296 |
297 | /// Read a json file directly into a specified D type.
298 | unittest {
299 | import std.path : buildPath;
300 | import std.uuid : randomUUID;
301 | import std.file : tempDir, write, mkdirRecurse;
302 |
303 | auto dir = buildPath(tempDir(), "jsonizer_readjson_test");
304 | mkdirRecurse(dir);
305 | auto file = buildPath(dir, randomUUID().toString);
306 |
307 | file.write("[1, 2, 3]");
308 |
309 | assert(file.readJSON!(int[]) == [ 1, 2, 3 ]);
310 | }
311 |
312 | /**
313 | * Read the contents of a json file directly into a `JSONValue`.
314 | * Params:
315 | * path = filesystem path to json file
316 | */
317 | auto readJSON(string path) {
318 | return parseJSON(readText(path));
319 | }
320 |
321 | /// Read a json file into a JSONValue.
322 | unittest {
323 | import std.path : buildPath;
324 | import std.uuid : randomUUID;
325 | import std.file : tempDir, write, mkdirRecurse;
326 |
327 | auto dir = buildPath(tempDir(), "jsonizer_readjson_test");
328 | mkdirRecurse(dir);
329 | auto file = buildPath(dir, randomUUID().toString);
330 |
331 | file.write("[1, 2, 3]");
332 |
333 | auto json = file.readJSON();
334 |
335 | assert(json.array[0].integer == 1);
336 | assert(json.array[1].integer == 2);
337 | assert(json.array[2].integer == 3);
338 | }
339 |
340 | deprecated("use fromJSON instead") {
341 | /// Deprecated: use `fromJSON` instead.
342 | T extract(T)(JSONValue json) {
343 | return json.fromJSON!T;
344 | }
345 | }
346 |
347 | private:
348 | void enforceJsonType(T)(JSONValue json, JSONType[] expected ...) {
349 | if (!expected.canFind(json.type)) {
350 | throw new JsonizeTypeException(typeid(T), json, expected);
351 | }
352 | }
353 |
354 | unittest {
355 | import std.exception : assertThrown, assertNotThrown;
356 | with (JSONType) {
357 | assertThrown!JsonizeTypeException(enforceJsonType!int(JSONValue("hi"), integer, uinteger));
358 | assertThrown!JsonizeTypeException(enforceJsonType!(bool[string])(JSONValue([ 5 ]), object));
359 | assertNotThrown(enforceJsonType!int(JSONValue(5), integer, uinteger));
360 | assertNotThrown(enforceJsonType!(bool[string])(JSONValue(["key": true]), object));
361 | }
362 | }
363 |
364 | // Internal implementation of fromJSON for user-defined types
365 | // If T is a nested class, pass the parent of type P
366 | // otherwise pass null for the parent
367 | T fromJSONImpl(T, P)(JSONValue json, P parent, in ref JsonizeOptions options) {
368 | static if (is(typeof(null) : T)) {
369 | if (json.type == JSONType.null_) { return null; }
370 | }
371 |
372 | // try constructing from a primitive type using a single-param constructor
373 | if (json.type != JSONType.object) {
374 | return invokePrimitiveCtor!T(json, parent);
375 | }
376 |
377 | static if (!isNested!T && is(T == class))
378 | {
379 | // if the class is identified in the json, construct an instance of the
380 | // specified type
381 | auto className = json.fromJSON!string(options.classKey, null);
382 | if (options.classMap) {
383 | if(auto tmp = options.classMap(className))
384 | className = tmp;
385 | }
386 | if (className && className != fullyQualifiedName!T) {
387 | auto handler = className in T._jsonizeCtors;
388 | assert(handler, className ~ " not registered in " ~ T.stringof);
389 | JsonizeOptions newopts = options;
390 | newopts.classKey = null; // don't recursively loop looking up class name
391 | return (*handler)(json, newopts);
392 | }
393 | }
394 |
395 | // next, try to find a contructor marked with @jsonize and call that
396 | static if (__traits(hasMember, T, "__ctor")) {
397 | alias Overloads = AliasSeq!(__traits(getOverloads, T, "__ctor"));
398 | foreach(overload ; Overloads) {
399 | static if (staticIndexOf!(jsonize, __traits(getAttributes, overload)) >= 0) {
400 | if (canSatisfyCtor!overload(json)) {
401 | return invokeCustomJsonCtor!(T, overload)(json, parent);
402 | }
403 | }
404 | }
405 |
406 | // no constructor worked, is default-construction an option?
407 | static if(!hasDefaultCtor!T) {
408 | // not default-constructable, need to fail here
409 | alias ctors = Filter!(isJsonized, __traits(getOverloads, T, "__ctor"));
410 | JsonizeConstructorException.doThrow!(T, ctors)(json);
411 | }
412 | }
413 |
414 | // if no @jsonized ctor, try to use a default ctor and populate the fields
415 | static if(hasDefaultCtor!T) {
416 | return invokeDefaultCtor!T(json, parent, options);
417 | }
418 |
419 | assert(0, "all attempts at deserializing " ~ fullyQualifiedName!T ~ " failed.");
420 | }
421 |
422 | // return true if keys can satisfy parameter names
423 | bool canSatisfyCtor(alias Ctor)(JSONValue json) {
424 | import std.meta : aliasSeqOf;
425 | auto obj = json.object;
426 | alias Params = ParameterIdentifierTuple!Ctor;
427 | alias Types = ParameterTypeTuple!Ctor;
428 | alias Defaults = ParameterDefaultValueTuple!Ctor;
429 | foreach(i ; aliasSeqOf!(Params.length.iota)) {
430 | if (Params[i] !in obj && typeid(Defaults[i]) == typeid(void)) {
431 | return false; // param had no default value and was not specified
432 | }
433 | }
434 | return true;
435 | }
436 |
437 | T invokeCustomJsonCtor(T, alias Ctor, P)(JSONValue json, P parent) {
438 | import std.meta : aliasSeqOf;
439 | enum params = ParameterIdentifierTuple!(Ctor);
440 | alias defaults = ParameterDefaultValueTuple!(Ctor);
441 | alias Types = ParameterTypeTuple!(Ctor);
442 | Tuple!(Types) args;
443 | foreach(i ; aliasSeqOf!(params.length.iota)) {
444 | enum paramName = params[i];
445 | if (paramName in json.object) {
446 | args[i] = json.object[paramName].fromJSON!(Types[i]);
447 | }
448 | else { // no value specified in json
449 | static if (is(defaults[i] == void)) {
450 | enforce(0, "parameter " ~ paramName ~ " has no default value and was not specified");
451 | }
452 | else {
453 | args[i] = defaults[i];
454 | }
455 | }
456 | }
457 | static if (isNested!T)
458 | return parent.new T(args.expand);
459 | else static if (is(T == class))
460 | return new T(args.expand);
461 | else
462 | return T(args.expand);
463 | }
464 |
465 | T invokeDefaultCtor(T, P)(JSONValue json, P parent, JsonizeOptions options) {
466 | T obj;
467 |
468 | static if (isNested!T) {
469 | obj = parent.new T;
470 | }
471 | else static if (is(T == struct)) {
472 | obj = T.init;
473 | }
474 | else {
475 | obj = new T;
476 | }
477 |
478 | populate(obj, json, options);
479 | return obj;
480 | }
481 |
482 | alias isJsonized(alias member) = hasUDA!(member, jsonize);
483 |
484 | unittest {
485 | static class Foo {
486 | @jsonize int i;
487 | @jsonize("s") string _s;
488 | @jsonize @property string sprop() { return _s; }
489 | @jsonize @property void sprop(string str) { _s = str; }
490 | float f;
491 | @property int iprop() { return i; }
492 |
493 | @jsonize this(string s) { }
494 | this(int i) { }
495 | }
496 |
497 | Foo f;
498 | static assert(isJsonized!(__traits(getMember, f, "i")));
499 | static assert(isJsonized!(__traits(getMember, f, "_s")));
500 | static assert(isJsonized!(__traits(getMember, f, "sprop")));
501 | static assert(isJsonized!(__traits(getMember, f, "i")));
502 | static assert(!isJsonized!(__traits(getMember, f, "f")));
503 | static assert(!isJsonized!(__traits(getMember, f, "iprop")));
504 |
505 | import std.typetuple : Filter;
506 | static assert(Filter!(isJsonized, __traits(getOverloads, Foo, "__ctor")).length == 1);
507 | }
508 |
509 | T invokePrimitiveCtor(T, P)(JSONValue json, P parent) {
510 | static if (__traits(hasMember, T, "__ctor")) {
511 | foreach(overload ; __traits(getOverloads, T, "__ctor")) {
512 | alias Types = ParameterTypeTuple!overload;
513 |
514 | // look for an @jsonized ctor with a single parameter
515 | static if (hasUDA!(overload, jsonize) && Types.length == 1) {
516 | return construct!T(parent, json.fromJSON!(Types[0]));
517 | }
518 | }
519 | }
520 |
521 | assert(0, "No primitive ctor for type " ~ T.stringof);
522 | }
523 |
524 | void populate(T)(ref T obj, JSONValue json, in JsonizeOptions opt) {
525 | string[] missingKeys;
526 | uint fieldsFound = 0;
527 |
528 | foreach (member ; T._membersWithUDA!jsonize) {
529 | string key = jsonKey!(T, member);
530 |
531 | auto required = JsonizeIn.unspecified;
532 | foreach (attr ; T._getUDAs!(member, jsonize))
533 | if (attr.perform_in != JsonizeIn.unspecified)
534 | required = attr.perform_in;
535 |
536 | if (required == JsonizeIn.no) continue;
537 |
538 | if (auto jsonval = key in json.object) {
539 | ++fieldsFound;
540 | alias MemberType = T._writeMemberType!member;
541 |
542 | static if (!is(MemberType == void)) {
543 | static if (isAggregateType!MemberType && isNested!MemberType)
544 | auto val = fromJSONImpl!Inner(*jsonval, obj, opt);
545 | else {
546 | auto val = fromJSON!MemberType(*jsonval, opt);
547 | }
548 |
549 | obj._writeMember!(MemberType, member)(val);
550 | }
551 | }
552 | else {
553 | if (required == JsonizeIn.yes) missingKeys ~= key;
554 | }
555 | }
556 |
557 | string[] extraKeys;
558 | if (!T._jsonizeIgnoreExtra && fieldsFound < json.object.length)
559 | extraKeys = json.object.keys.filter!(x => x.isUnknownKey!T).array;
560 |
561 | if (missingKeys.length > 0 || extraKeys.length > 0)
562 | throw new JsonizeMismatchException(typeid(T), extraKeys, missingKeys);
563 | }
564 |
565 | bool isUnknownKey(T)(string key) {
566 | foreach (member ; T._membersWithUDA!jsonize)
567 | if (jsonKey!(T, member) == key)
568 | return false;
569 |
570 | return true;
571 | }
572 |
573 | T construct(T, P, Params ...)(P parent, Params params) {
574 | static if (!is(P == typeof(null))) {
575 | return parent.new T(params);
576 | }
577 | else static if (is(typeof(T(params)) == T)) {
578 | return T(params);
579 | }
580 | else static if (is(typeof(new T(params)) == T)) {
581 | return new T(params);
582 | }
583 | else {
584 | static assert(0, "Cannot construct");
585 | }
586 | }
587 |
588 | unittest {
589 | static struct Foo {
590 | this(int i) { this.i = i; }
591 | int i;
592 | }
593 |
594 | assert(construct!Foo(null).i == 0);
595 | assert(construct!Foo(null, 4).i == 4);
596 | assert(!__traits(compiles, construct!Foo("asd")));
597 | }
598 |
599 | unittest {
600 | static class Foo {
601 | this(int i) { this.i = i; }
602 |
603 | this(int i, string s) {
604 | this.i = i;
605 | this.s = s;
606 | }
607 |
608 | int i;
609 | string s;
610 | }
611 |
612 | assert(construct!Foo(null, 4).i == 4);
613 | assert(construct!Foo(null, 4, "asdf").s == "asdf");
614 | assert(!__traits(compiles, construct!Foo("asd")));
615 | }
616 |
617 | unittest {
618 | class Foo {
619 | class Bar {
620 | int i;
621 | this(int i) { this.i = i; }
622 | }
623 | }
624 |
625 | auto f = new Foo;
626 | assert(construct!(Foo.Bar)(f, 2).i == 2);
627 | }
628 |
629 | template hasDefaultCtor(T) {
630 | static if (isNested!T) {
631 | alias P = typeof(__traits(parent, T).init);
632 | enum hasDefaultCtor = is(typeof(P.init.new T()) == T);
633 | }
634 | else {
635 | enum hasDefaultCtor = is(typeof(T()) == T) || is(typeof(new T()) == T);
636 | }
637 | }
638 |
639 | version(unittest) {
640 | struct S1 { }
641 | struct S2 { this(int i) { } }
642 | struct S3 { @disable this(); }
643 |
644 | class C1 { }
645 | class C2 { this(string s) { } }
646 | class C3 { class Inner { } }
647 | class C4 { class Inner { this(int i); } }
648 | }
649 |
650 | unittest {
651 | static assert( hasDefaultCtor!S1);
652 | static assert( hasDefaultCtor!S2);
653 | static assert(!hasDefaultCtor!S3);
654 |
655 | static assert( hasDefaultCtor!C1);
656 | static assert(!hasDefaultCtor!C2);
657 |
658 | static assert( hasDefaultCtor!C3);
659 | static assert( hasDefaultCtor!(C3.Inner));
660 | static assert(!hasDefaultCtor!(C4.Inner));
661 | }
662 |
663 | template hasCustomJsonCtor(T) {
664 | static if (__traits(hasMember, T, "__ctor")) {
665 | alias Overloads = AliasSeq!(__traits(getOverloads, T, "__ctor"));
666 |
667 | enum test(alias fn) = staticIndexOf!(jsonize, __traits(getAttributes, fn)) >= 0;
668 |
669 | enum hasCustomJsonCtor = anySatisfy!(test, Overloads);
670 | }
671 | else {
672 | enum hasCustomJsonCtor = false;
673 | }
674 | }
675 |
676 | unittest {
677 | static struct S1 { }
678 | static struct S2 { this(int i); }
679 | static struct S3 { @jsonize this(int i); }
680 | static struct S4 { this(float f); @jsonize this(int i); }
681 |
682 | static assert(!hasCustomJsonCtor!S1);
683 | static assert(!hasCustomJsonCtor!S2);
684 | static assert( hasCustomJsonCtor!S3);
685 | static assert( hasCustomJsonCtor!S4);
686 |
687 | static class C1 { }
688 | static class C2 { this() {} }
689 | static class C3 { @jsonize this() {} }
690 | static class C4 { @jsonize this(int i); this(float f); }
691 |
692 | static assert(!hasCustomJsonCtor!C1);
693 | static assert(!hasCustomJsonCtor!C2);
694 | static assert( hasCustomJsonCtor!C3);
695 | static assert( hasCustomJsonCtor!C4);
696 | }
697 |
--------------------------------------------------------------------------------
/src/jsonizer/jsonize.d:
--------------------------------------------------------------------------------
1 | /**
2 | * Enables marking user-defined types for JSON serialization.
3 | *
4 | * Authors: rcorre
5 | * License: MIT
6 | * Copyright: Copyright © 2015, rcorre
7 | * Date: 3/23/15
8 | */
9 | module jsonizer.jsonize;
10 |
11 | import jsonizer.common;
12 |
13 | /**
14 | * Enable `fromJSON`/`toJSON` support for the type this is mixed in to.
15 | *
16 | * In order for fields to be (de)serialized, they must be annotated with
17 | * `jsonize` (in addition to having the mixin within the type).
18 | *
19 | * This mixin will _not_ recursively apply to nested types. If a nested type is
20 | * to be serialized, it must have `JsonizeMe` mixed in as well.
21 | * Params:
22 | * ignoreExtra = whether to silently ignore json keys that do not map to serialized members
23 | */
24 | mixin template JsonizeMe(JsonizeIgnoreExtraKeys ignoreExtra = JsonizeIgnoreExtraKeys.yes) {
25 | static import std.json;
26 |
27 | enum type = typeof(this).stringof;
28 |
29 | template _membersWithUDA(uda) {
30 | import std.meta : Erase, Filter;
31 | import std.traits : isSomeFunction, hasUDA;
32 | import std.string : startsWith;
33 |
34 | template include(string name) {
35 | // filter out inaccessible members, such as those with @disable
36 | static if (__traits(compiles, mixin(type~"."~name))) {
37 | enum isReserved = name.startsWith("__");
38 |
39 | enum isInstanceField =
40 | __traits(compiles, mixin(type~"."~name~".offsetof"));
41 |
42 | // the &this.name check makes sure this is not an alias
43 | enum isInstanceMethod =
44 | __traits(compiles, mixin("&"~type~"."~name)) &&
45 | isSomeFunction!(mixin(type~"."~name)) &&
46 | !__traits(isStaticFunction, mixin(type~"."~name));
47 |
48 | static if ((isInstanceField || isInstanceMethod) && !isReserved)
49 | enum include = hasUDA!(mixin(type~"."~name), uda);
50 | else
51 | enum include = false;
52 | }
53 | else
54 | enum include = false;
55 | }
56 |
57 | enum members = Erase!("this", __traits(allMembers, typeof(this)));
58 | alias _membersWithUDA = Filter!(include, members);
59 | }
60 |
61 | template _getUDAs(string name, alias uda) {
62 | import std.meta : Filter;
63 | import std.traits : getUDAs;
64 | enum isValue(alias T) = is(typeof(T));
65 | alias _getUDAs = Filter!(isValue, getUDAs!(mixin(type~"."~name), uda));
66 | }
67 |
68 | template _writeMemberType(string name) {
69 | import std.meta : Filter, AliasSeq;
70 | import std.traits : Parameters;
71 | alias overloads = AliasSeq!(__traits(getOverloads, typeof(this), name));
72 | enum hasOneArg(alias f) = Parameters!f.length == 1;
73 | alias setters = Filter!(hasOneArg, overloads);
74 | void tryassign()() { mixin("this."~name~"=this."~name~";"); }
75 |
76 | static if (setters.length)
77 | alias _writeMemberType = Parameters!(setters[0]);
78 | else static if (__traits(compiles, tryassign()))
79 | alias _writeMemberType = typeof(mixin(type~"."~name));
80 | else
81 | alias _writeMemberType = void;
82 | }
83 |
84 | auto _readMember(string name)() {
85 | return __traits(getMember, this, name);
86 | }
87 |
88 | void _writeMember(T, string name)(T val) {
89 | __traits(getMember, this, name) = val;
90 | }
91 |
92 | static import std.json;
93 | static import jsonizer.common;
94 | alias _jsonizeIgnoreExtra = ignoreExtra;
95 | private alias constructor =
96 | typeof(this) function(std.json.JSONValue,
97 | in ref jsonizer.common.JsonizeOptions);
98 | static constructor[string] _jsonizeCtors;
99 |
100 | static if (is(typeof(this) == class)) {
101 | static this() {
102 | import std.traits : BaseClassesTuple, fullyQualifiedName;
103 | import jsonizer.fromjson;
104 | enum name = fullyQualifiedName!(typeof(this));
105 | foreach (base ; BaseClassesTuple!(typeof(this)))
106 | static if (__traits(hasMember, base, "_jsonizeCtors"))
107 | base._jsonizeCtors[name] = &_fromJSON!(typeof(this));
108 | }
109 | }
110 | }
111 |
112 | unittest {
113 | static struct attr { string s; }
114 | static struct S {
115 | mixin JsonizeMe;
116 | @attr this(int i) { }
117 | @attr this(this) { }
118 | @attr ~this() { }
119 | @attr int a;
120 | @attr static int b;
121 | @attr void c() { }
122 | @attr static void d() { }
123 | @attr int e(string s) { return 1; }
124 | @attr static int f(string s) { return 1; }
125 | @attr("foo") int g;
126 | @attr("foo") static int h;
127 | int i;
128 | static int j;
129 | void k() { };
130 | static void l() { };
131 | alias Int = int;
132 | enum s = 5;
133 | }
134 |
135 | static assert ([S._membersWithUDA!attr] == ["a", "c", "e", "g"]);
136 | }
137 |
138 | unittest {
139 | struct attr { string s; }
140 | struct Outer {
141 | mixin JsonizeMe;
142 | @attr int a;
143 | struct Inner {
144 | mixin JsonizeMe;
145 | @attr this(int i) { }
146 | @attr this(this) { }
147 | @attr int b;
148 | }
149 | }
150 |
151 | static assert ([Outer._membersWithUDA!attr] == ["a"]);
152 | static assert ([Outer.Inner._membersWithUDA!attr] == ["b"]);
153 | }
154 |
155 | unittest {
156 | struct attr { string s; }
157 | struct A {
158 | mixin JsonizeMe;
159 | @disable this();
160 | @disable this(this);
161 | @attr int a;
162 | }
163 |
164 | static assert ([A._membersWithUDA!attr] == ["a"]);
165 | }
166 |
167 | unittest {
168 | struct attr { string s; }
169 |
170 | static class A {
171 | mixin JsonizeMe;
172 | @attr int a;
173 | @attr string b() { return "hi"; }
174 | string c() { return "hi"; }
175 | }
176 |
177 | static assert ([A._membersWithUDA!attr] == ["a", "b"]);
178 |
179 | static class B : A { mixin JsonizeMe; }
180 |
181 | static assert ([B._membersWithUDA!attr] == ["a", "b"]);
182 |
183 | static class C : A {
184 | mixin JsonizeMe;
185 | @attr int d;
186 | }
187 |
188 | static assert ([C._membersWithUDA!attr] == ["d", "a", "b"]);
189 |
190 | static class D : A {
191 | mixin JsonizeMe;
192 | @disable int a;
193 | }
194 |
195 | static assert ([D._membersWithUDA!attr] == ["b"]);
196 | }
197 |
198 | // Validate name conflicts (issue #36)
199 | unittest {
200 | static struct attr { string s; }
201 | static struct S {
202 | mixin JsonizeMe;
203 | @attr("foo") string name, key;
204 | }
205 |
206 | static assert([S._membersWithUDA!attr] == ["name", "key"]);
207 | static assert([S._getUDAs!("name", attr)] == [attr("foo")]);
208 | static assert([S._getUDAs!("key", attr)] == [attr("foo")]);
209 | }
210 |
211 | // #40: Can't deserialize both as exact type and as part of a hierarchy
212 | unittest
213 | {
214 | import jsonizer.fromjson;
215 | import jsonizer.tojson;
216 |
217 | static class Base
218 | {
219 | mixin JsonizeMe;
220 | @jsonize("class") string className() { return this.classinfo.name; }
221 | }
222 | static class Derived : Base
223 | {
224 | mixin JsonizeMe;
225 | }
226 |
227 | auto a = new Derived();
228 | auto b = a.toJSON.fromJSON!Derived;
229 | assert(b !is null);
230 | }
231 |
--------------------------------------------------------------------------------
/src/jsonizer/package.d:
--------------------------------------------------------------------------------
1 | /**
2 | * Import all public modules for jsonizer.
3 | *
4 | * Authors: rcorre
5 | * License: MIT
6 | * Copyright: Copyright © 2015, rcorre
7 | * Date: 3/23/15
8 | */
9 | module jsonizer;
10 |
11 | public import jsonizer.tojson;
12 | public import jsonizer.fromjson;
13 | public import jsonizer.jsonize;
14 | public import jsonizer.common;
15 | public import jsonizer.exceptions;
16 |
17 | /// object serialization -- fields only
18 | unittest {
19 | import std.math : approxEqual;
20 |
21 | static class Fields {
22 | // classes must have a no-args constructor
23 | // or a constructor marked with @jsonize (see later example)
24 | this() { }
25 |
26 | this(int iVal, float fVal, string sVal, int[] aVal, string noJson) {
27 | i = iVal;
28 | f = fVal;
29 | s = sVal;
30 | a = aVal;
31 | dontJsonMe = noJson;
32 | }
33 |
34 | mixin JsonizeMe;
35 |
36 | @jsonize { // fields to jsonize -- test different access levels
37 | public int i;
38 | protected float f;
39 | public int[] a;
40 | private string s;
41 | }
42 | string dontJsonMe;
43 |
44 | override bool opEquals(Object o) {
45 | auto other = cast(Fields) o;
46 | return i == other.i && s == other.s && a == other.a && f.approxEqual(other.f);
47 | }
48 | }
49 |
50 | auto obj = new Fields(1, 4.2, "tally ho!", [9, 8, 7, 6], "blarg");
51 | auto json = toJSON!Fields(obj);
52 |
53 | assert(json.object["i"].integer == 1);
54 | assert(json.object["f"].floating.approxEqual(4.2));
55 | assert(json.object["s"].str == "tally ho!");
56 | assert(json.object["a"].array[0].integer == 9);
57 | assert("dontJsonMe" !in json.object);
58 |
59 | // reconstruct from json
60 | auto r = fromJSON!Fields(json);
61 | assert(r.i == 1);
62 | assert(r.f.approxEqual(4.2));
63 | assert(r.s == "tally ho!");
64 | assert(r.a == [9, 8, 7, 6]);
65 | assert(r.dontJsonMe is null);
66 |
67 | // array of objects
68 | auto a = [
69 | new Fields(1, 4.2, "tally ho!", [9, 8, 7, 6], "blarg"),
70 | new Fields(7, 42.2, "yea merrily", [1, 4, 6, 4], "asparagus")
71 | ];
72 |
73 | // serialize array of user objects to json
74 | auto jsonArray = toJSON!(Fields[])(a);
75 | // reconstruct from json
76 | assert(fromJSON!(Fields[])(jsonArray) == a);
77 | }
78 |
79 | /// object serialization with properties
80 | unittest {
81 | import std.math : approxEqual;
82 |
83 | static class Props {
84 | this() { } // class must have a no-args ctor
85 |
86 | this(int iVal, float fVal, string sVal, string noJson) {
87 | _i = iVal;
88 | _f = fVal;
89 | _s = sVal;
90 | _dontJsonMe = noJson;
91 | }
92 |
93 | mixin JsonizeMe;
94 |
95 | @property {
96 | // jsonize ref property accessing private field
97 | @jsonize ref int i() { return _i; }
98 | // jsonize property with non-trivial get/set methods
99 | @jsonize float f() { return _f - 3; } // the jsonized value will equal _f - 3
100 | float f(float val) { return _f = val + 5; } // 5 will be added to _f when retrieving from json
101 | // don't jsonize these properties
102 | ref string s() { return _s; }
103 | ref string dontJsonMe() { return _dontJsonMe; }
104 | }
105 |
106 | private:
107 | int _i;
108 | float _f;
109 | @jsonize string _s;
110 | string _dontJsonMe;
111 | }
112 |
113 | auto obj = new Props(1, 4.2, "tally ho!", "blarg");
114 | auto json = toJSON(obj);
115 |
116 | assert(json.object["i"].integer == 1);
117 | assert(json.object["f"].floating.approxEqual(4.2 - 3.0)); // property should have subtracted 3 on retrieval
118 | assert(json.object["_s"].str == "tally ho!");
119 | assert("dontJsonMe" !in json.object);
120 |
121 | auto r = fromJSON!Props(json);
122 | assert(r.i == 1);
123 | assert(r._f.approxEqual(4.2 - 3.0 + 5.0)); // property accessor should add 5
124 | assert(r._s == "tally ho!");
125 | assert(r.dontJsonMe is null);
126 | }
127 |
128 | /// object serialization with custom constructor
129 | unittest {
130 | import std.conv : to;
131 | import std.json : parseJSON;
132 | import std.math : approxEqual;
133 | import jsonizer.tojson : toJSON;
134 |
135 | static class Custom {
136 | mixin JsonizeMe;
137 |
138 | this(int i) {
139 | _i = i;
140 | _s = "something";
141 | _f = 10.2;
142 | }
143 |
144 | @jsonize this(int _i, string _s, float _f = 20.2) {
145 | this._i = _i;
146 | this._s = _s ~ " jsonized";
147 | this._f = _f;
148 | }
149 |
150 | @jsonize this(double d) { // alternate ctor
151 | _f = d.to!float;
152 | _s = d.to!string;
153 | _i = d.to!int;
154 | }
155 |
156 | private:
157 | @jsonize {
158 | string _s;
159 | float _f;
160 | int _i;
161 | }
162 | }
163 |
164 | auto c = new Custom(12);
165 | auto json = toJSON(c);
166 | assert(json.object["_i"].integer == 12);
167 | assert(json.object["_s"].str == "something");
168 | assert(json.object["_f"].floating.approxEqual(10.2));
169 | auto c2 = fromJSON!Custom(json);
170 | assert(c2._i == 12);
171 | assert(c2._s == "something jsonized");
172 | assert(c2._f.approxEqual(10.2));
173 |
174 | // test alternate ctor
175 | json = parseJSON(`{"d" : 5}`);
176 | c = json.fromJSON!Custom;
177 | assert(c._f.approxEqual(5) && c._i == 5 && c._s == "5");
178 | }
179 |
180 | /// struct serialization
181 | unittest {
182 | import std.math : approxEqual;
183 |
184 | static struct S {
185 | mixin JsonizeMe;
186 |
187 | @jsonize {
188 | int x;
189 | float f;
190 | string s;
191 | }
192 | int dontJsonMe;
193 |
194 | this(int x, float f, string s, int noJson) {
195 | this.x = x;
196 | this.f = f;
197 | this.s = s;
198 | this.dontJsonMe = noJson;
199 | }
200 | }
201 |
202 | auto s = S(5, 4.2, "bogus", 7);
203 | auto json = toJSON(s); // serialize a struct
204 |
205 | assert(json.object["x"].integer == 5);
206 | assert(json.object["f"].floating.approxEqual(4.2));
207 | assert(json.object["s"].str == "bogus");
208 | assert("dontJsonMe" !in json.object);
209 |
210 | auto r = fromJSON!S(json);
211 | assert(r.x == 5);
212 | assert(r.f.approxEqual(4.2));
213 | assert(r.s == "bogus");
214 | assert(r.dontJsonMe == int.init);
215 | }
216 |
217 | /// json file I/O
218 | unittest {
219 | import std.file : remove;
220 | import jsonizer.fromjson : readJSON;
221 | import jsonizer.tojson : writeJSON;
222 |
223 | enum file = "test.json";
224 | scope(exit) remove(file);
225 |
226 | static struct Data {
227 | mixin JsonizeMe;
228 |
229 | @jsonize {
230 | int x;
231 | string s;
232 | float f;
233 | }
234 | }
235 |
236 | // write an array of user-defined structs
237 | auto array = [Data(5, "preposterous", 12.7), Data(8, "tesseract", -2.7), Data(5, "baby sloths", 102.7)];
238 | file.writeJSON(array);
239 | auto readBack = file.readJSON!(Data[]);
240 | assert(readBack == array);
241 |
242 | // now try an associative array
243 | auto aa = ["alpha": Data(27, "yams", 0), "gamma": Data(88, "spork", -99.999)];
244 | file.writeJSON(aa);
245 | auto aaReadBack = file.readJSON!(Data[string]);
246 | assert(aaReadBack == aa);
247 | }
248 |
249 | /// inheritance
250 | unittest {
251 | import std.math : approxEqual;
252 | static class Parent {
253 | mixin JsonizeMe;
254 | @jsonize int x;
255 | @jsonize string s;
256 | }
257 |
258 | static class Child : Parent {
259 | mixin JsonizeMe;
260 | @jsonize float f;
261 | }
262 |
263 | auto c = new Child;
264 | c.x = 5;
265 | c.s = "hello";
266 | c.f = 2.1;
267 |
268 | auto json = c.toJSON;
269 | assert(json.fromJSON!int("x") == 5);
270 | assert(json.fromJSON!string("s") == "hello");
271 | assert(json.fromJSON!float("f").approxEqual(2.1));
272 |
273 | auto child = json.fromJSON!Child;
274 | assert(child !is null);
275 | assert(child.x == 5 && child.s == "hello" && child.f.approxEqual(2.1));
276 |
277 | auto parent = json.fromJSON!Parent;
278 | assert(parent.x == 5 && parent.s == "hello");
279 | }
280 |
281 | /// inheritance with ctors
282 | unittest {
283 | import std.math : approxEqual;
284 | static class Parent {
285 | mixin JsonizeMe;
286 |
287 | @jsonize this(int x, string s) {
288 | _x = x;
289 | _s = s;
290 | }
291 |
292 | @jsonize @property {
293 | int x() { return _x; }
294 | string s() { return _s; }
295 | }
296 |
297 | private:
298 | int _x;
299 | string _s;
300 | }
301 |
302 | static class Child : Parent {
303 | mixin JsonizeMe;
304 |
305 | @jsonize this(int x, string s, float f) {
306 | super(x, s);
307 | _f = f;
308 | }
309 |
310 | @jsonize @property {
311 | float f() { return _f; }
312 | }
313 |
314 | private:
315 | float _f;
316 | }
317 |
318 | auto c = new Child(5, "hello", 2.1);
319 |
320 | auto json = c.toJSON;
321 | assert(json.fromJSON!int("x") == 5);
322 | assert(json.fromJSON!string("s") == "hello");
323 | assert(json.fromJSON!float("f").approxEqual(2.1));
324 |
325 | auto child = json.fromJSON!Child;
326 | assert(child.x == 5 && child.s == "hello" && child.f.approxEqual(2.1));
327 |
328 | auto parent = json.fromJSON!Parent;
329 | assert(parent.x == 5 && parent.s == "hello");
330 | }
331 |
332 | /// renamed members
333 | unittest {
334 | static class Bleh {
335 | mixin JsonizeMe;
336 | private {
337 | @jsonize("x") int _x;
338 | @jsonize("s") string _s;
339 | }
340 | }
341 |
342 | auto b = new Bleh;
343 | b._x = 5;
344 | b._s = "blah";
345 |
346 | auto json = b.toJSON;
347 |
348 | assert(json.fromJSON!int("x") == 5);
349 | assert(json.fromJSON!string("s") == "blah");
350 |
351 | auto reconstruct = json.fromJSON!Bleh;
352 | assert(reconstruct._x == b._x && reconstruct._s == b._s);
353 | }
354 |
355 | /// type inference
356 | unittest {
357 | import std.json : parseJSON;
358 | import std.string : format;
359 | import std.traits : fullyQualifiedName;
360 |
361 | static class TestComponent {
362 | mixin JsonizeMe;
363 | @jsonize int c;
364 | }
365 |
366 | static class TestCompA : TestComponent {
367 | mixin JsonizeMe;
368 | @jsonize int a;
369 | }
370 |
371 | static class TestCompB : TestComponent {
372 | mixin JsonizeMe;
373 | @jsonize string b;
374 | }
375 |
376 | string classKeyA = fullyQualifiedName!TestCompA;
377 | string classKeyB = fullyQualifiedName!TestCompB;
378 |
379 | auto data = `[
380 | {
381 | "class": "%s",
382 | "c": 1,
383 | "a": 5
384 | },
385 | {
386 | "class": "%s",
387 | "c": 2,
388 | "b": "hello"
389 | }
390 | ]`.format(classKeyA, classKeyB).parseJSON.fromJSON!(TestComponent[]);
391 |
392 | auto a = cast(TestCompA) data[0];
393 | auto b = cast(TestCompB) data[1];
394 |
395 | assert(a !is null && a.c == 1 && a.a == 5);
396 | assert(b !is null && b.c == 2 && b.b == "hello");
397 | }
398 |
399 | /// type inference with custom type key
400 | unittest {
401 | import std.string : format;
402 | import std.traits : fullyQualifiedName;
403 |
404 | static class TestComponent {
405 | mixin JsonizeMe;
406 | @jsonize int c;
407 | }
408 |
409 | static class TestCompA : TestComponent {
410 | mixin JsonizeMe;
411 | @jsonize int a;
412 | }
413 |
414 | static class TestCompB : TestComponent {
415 | mixin JsonizeMe;
416 | @jsonize string b;
417 | }
418 |
419 | // use "type" instead of "class" to identify dynamic type
420 | JsonizeOptions options;
421 | options.classKey = "type";
422 |
423 | // need to use these because unittest is assigned weird name
424 | // normally would just be "modulename.classname"
425 | string classKeyA = fullyQualifiedName!TestCompA;
426 | string classKeyB = fullyQualifiedName!TestCompB;
427 |
428 | auto data = `[
429 | {
430 | "type": "%s",
431 | "c": 1,
432 | "a": 5
433 | },
434 | {
435 | "type": "%s",
436 | "c": 2,
437 | "b": "hello"
438 | }
439 | ]`.format(classKeyA, classKeyB)
440 | .fromJSONString!(TestComponent[])(options);
441 |
442 | auto a = cast(TestCompA) data[0];
443 | auto b = cast(TestCompB) data[1];
444 | assert(a !is null && a.c == 1 && a.a == 5);
445 | assert(b !is null && b.c == 2 && b.b == "hello");
446 | }
447 |
448 | //test the class map
449 | unittest {
450 | import std.string : format;
451 | import std.traits : fullyQualifiedName;
452 |
453 | static class TestComponent {
454 | mixin JsonizeMe;
455 | @jsonize int c;
456 | }
457 |
458 | static class TestCompA : TestComponent {
459 | mixin JsonizeMe;
460 | @jsonize int a;
461 | }
462 |
463 | static class TestCompB : TestComponent {
464 | mixin JsonizeMe;
465 | @jsonize string b;
466 | }
467 |
468 | // use "type" instead of "class" to identify dynamic type
469 | JsonizeOptions options;
470 | options.classKey = "type";
471 |
472 | const string wrongName = "unrelated";
473 |
474 | string[string] classMap = [
475 | TestCompA.stringof : fullyQualifiedName!TestCompA,
476 | TestCompB.stringof : fullyQualifiedName!TestCompB,
477 | wrongName : fullyQualifiedName!TestCompA
478 | ];
479 |
480 | options.classMap = delegate string(string rawKey) {
481 | if(auto val = rawKey in classMap)
482 | return *val;
483 | else
484 | return null;
485 | };
486 |
487 | auto data = `[
488 | {
489 | "type": "%s",
490 | "c": 1,
491 | "a": 5
492 | },
493 | {
494 | "type": "%s",
495 | "c": 2,
496 | "b": "hello"
497 | },
498 | {
499 | "type": "%s",
500 | "c": 3,
501 | "a": 12
502 | }
503 | ]`.format(TestCompA.stringof, TestCompB.stringof, wrongName)
504 | .fromJSONString!(TestComponent[])(options);
505 |
506 | auto a = cast(TestCompA) data[0];
507 | auto b = cast(TestCompB) data[1];
508 | auto c = cast(TestCompA) data[2];
509 | assert(a !is null && a.c == 1 && a.a == 5);
510 | assert(b !is null && b.c == 2 && b.b == "hello");
511 | assert(c !is null && c.c == 3 && c.a == 12);
512 | }
513 |
514 | // Validate issue #20:
515 | // Unable to de-jsonize a class when a construct is marked @jsonize.
516 | unittest {
517 | import std.json : parseJSON;
518 | import std.algorithm : canFind;
519 | import std.exception : collectException;
520 | import jsonizer.common : jsonize;
521 | import jsonizer.exceptions : JsonizeConstructorException;
522 | import jsonizer.fromjson : fromJSON;
523 | import jsonizer.jsonize : JsonizeMe;
524 |
525 | static class A {
526 | mixin JsonizeMe;
527 | private const int a;
528 |
529 | this(float f) {
530 | a = 0;
531 | }
532 |
533 | @jsonize this(int a) {
534 | this.a = a;
535 | }
536 |
537 | @jsonize this(string s, float f) {
538 | a = 0;
539 | }
540 | }
541 |
542 | auto ex = collectException!JsonizeConstructorException(`{}`.parseJSON.fromJSON!A);
543 | assert(ex !is null, "failure to match @jsonize'd constructors should throw");
544 | assert(ex.msg.canFind("(int a)") && ex.msg.canFind("(string s, float f)"),
545 | "JsonizeConstructorException message should contain attempted constructors");
546 | assert(!ex.msg.canFind("(float f)"),
547 | "JsonizeConstructorException message should not contain non-jsonized constructors");
548 | }
549 |
550 | // Validate issue #17:
551 | // Unable to construct class containing private (not marked with @jsonize) types.
552 | unittest {
553 | import std.json : parseJSON;
554 | import jsonizer;
555 |
556 | static class A {
557 | mixin JsonizeMe;
558 |
559 | private int a;
560 |
561 | @jsonize public this(int a) {
562 | this.a = a;
563 | }
564 | }
565 |
566 | auto json = `{ "a": 5}`.parseJSON;
567 | auto a = fromJSON!A(json);
568 |
569 | assert(a.a == 5);
570 | }
571 |
572 | // Validate issue #18:
573 | // Unable to construct class with const types.
574 | unittest {
575 | import std.json : parseJSON;
576 | import jsonizer;
577 |
578 | static class A {
579 | mixin JsonizeMe;
580 |
581 | const int a;
582 |
583 | @jsonize public this(int a) {
584 | this.a = a;
585 | }
586 | }
587 |
588 | auto json = `{ "a": 5}`.parseJSON;
589 | auto a = fromJSON!A(json);
590 |
591 | assert(a.a == 5);
592 | }
593 |
594 | // Validate issue #19:
595 | // Unable to construct class containing private (not marked with @jsonize) types.
596 | unittest {
597 | import std.json : parseJSON;
598 | import jsonizer;
599 |
600 | static class A {
601 | mixin JsonizeMe;
602 |
603 | alias Integer = int;
604 | Integer a;
605 |
606 | @jsonize public this(Integer a) {
607 | this.a = a;
608 | }
609 | }
610 |
611 | auto json = `{ "a": 5}`.parseJSON;
612 | auto a = fromJSON!A(json);
613 |
614 | assert(a.a == 5);
615 | }
616 |
617 | unittest {
618 | import std.json : parseJSON;
619 | import jsonizer;
620 |
621 | static struct A
622 | {
623 | mixin JsonizeMe;
624 | @jsonize int a;
625 | @jsonize(Jsonize.opt) string attr;
626 | @jsonize(JsonizeIn.opt) string attr2;
627 | }
628 |
629 | auto a = A(5);
630 | assert(a == a.toJSON.fromJSON!A);
631 | assert(a.toJSON == `{ "a":5, "attr2":"" }`.parseJSON);
632 | assert(a.toJSON != `{ "a":5, "attr":"", "attr2":"" }`.parseJSON);
633 | a.attr = "hello";
634 | assert(a == a.toJSON.fromJSON!A);
635 | assert(a.toJSON == `{ "a":5, "attr":"hello", "attr2":"" }`.parseJSON);
636 | a.attr2 = "world";
637 | assert(a == a.toJSON.fromJSON!A);
638 | assert(a.toJSON == `{ "a":5, "attr":"hello", "attr2":"world" }`.parseJSON);
639 | a.attr = "";
640 | assert(a == a.toJSON.fromJSON!A);
641 | assert(a.toJSON == `{ "a":5, "attr2":"world" }`.parseJSON);
642 | }
643 |
644 | unittest {
645 | import std.json : parseJSON;
646 | import jsonizer;
647 |
648 | static struct A
649 | {
650 | mixin JsonizeMe;
651 | @jsonize int a;
652 | @disable int opEquals( ref const(A) );
653 | }
654 |
655 | static assert(!is(typeof(A.init==A.init)));
656 |
657 | static struct B
658 | {
659 | mixin JsonizeMe;
660 | @jsonize(Jsonize.opt) A a;
661 | }
662 |
663 | auto b = B(A(10));
664 | assert(b.a.a == 10);
665 | assert(b.a.a == (b.toJSON.fromJSON!B).a.a);
666 | assert(b.toJSON == `{"a":{"a":10}}`.parseJSON);
667 | b.a.a = 0;
668 | assert(b.a.a == int.init );
669 | assert(b.a.a == (b.toJSON.fromJSON!B).a.a);
670 | assert(b.toJSON == "{}".parseJSON);
671 | }
672 |
673 | unittest {
674 | import std.json : parseJSON;
675 | import std.exception;
676 | import jsonizer;
677 |
678 | static struct T
679 | {
680 | mixin JsonizeMe;
681 | @jsonize(Jsonize.opt)
682 | {
683 | int a;
684 | @jsonize(JsonizeOut.no,JsonizeIn.yes) string b;
685 | @jsonize(JsonizeOut.yes,JsonizeIn.no) string c;
686 | }
687 | }
688 |
689 | auto t = T(5);
690 | assertThrown(t.toJSON.fromJSON!T);
691 | assert(t.toJSON == `{ "a":5, "c":"" }`.parseJSON);
692 | t.b = "hello";
693 | assert(t == `{ "a":5, "b":"hello" }`.parseJSON.fromJSON!T);
694 | t.c = "world";
695 | assert(t.toJSON == `{ "a":5, "c":"world" }`.parseJSON);
696 | t.a = 0;
697 | assert(t.toJSON == `{ "c":"world" }`.parseJSON);
698 | auto t2 = `{ "b":"hello", "c":"okda" }`.parseJSON.fromJSON!T;
699 | assert(t.a == t2.a);
700 | assert(t.b == t2.b);
701 | assert(t2.c == "");
702 | }
703 |
704 | // issue 29: Can't have static fields in JsonizeMe type; doesn't compile
705 | unittest {
706 | import std.json : parseJSON;
707 | static class A {
708 | mixin JsonizeMe;
709 | static string s;
710 | static string str() { return "s"; }
711 | @jsonize int a;
712 | }
713 | auto a = new A;
714 | a.a = 5;
715 | assert(a.toJSON == `{ "a":5 }`.parseJSON);
716 | }
717 |
--------------------------------------------------------------------------------
/src/jsonizer/tojson.d:
--------------------------------------------------------------------------------
1 | /**
2 | * Contains functions for serializing JSON data.
3 | *
4 | * Authors: rcorre
5 | * License: MIT
6 | * Copyright: Copyright © 2015, rcorre
7 | * Date: 3/23/15
8 | */
9 | module jsonizer.tojson;
10 |
11 | import std.json;
12 | import std.traits;
13 | import std.conv;
14 | import std.file;
15 | import std.exception;
16 | import std.typecons;
17 | import jsonizer.common;
18 |
19 | /// convert a JSONValue to a JSONValue (identity)
20 | JSONValue toJSON(JSONValue val) {
21 | return val;
22 | }
23 |
24 | /// convert a bool to a JSONValue
25 | JSONValue toJSON(T : bool)(T val) {
26 | return JSONValue(val);
27 | }
28 |
29 | /// Serialize a boolean.
30 | unittest {
31 | assert(false.toJSON == JSONValue(false));
32 | assert(true.toJSON == JSONValue(true));
33 | }
34 |
35 | /// convert a string to a JSONValue
36 | JSONValue toJSON(T : string)(T val) {
37 | return JSONValue(val);
38 | }
39 |
40 | /// Serialize a string.
41 | unittest {
42 | assert("bork".toJSON == JSONValue("bork"));
43 | }
44 |
45 | /// convert a floating point value to a JSONValue
46 | JSONValue toJSON(T : real)(T val) if (!is(T == enum)) {
47 | return JSONValue(val);
48 | }
49 |
50 | /// Serialize a floating-point value.
51 | unittest {
52 | assert(4.1f.toJSON == JSONValue(4.1f));
53 | }
54 |
55 | /// convert a signed integer to a JSONValue
56 | JSONValue toJSON(T : long)(T val) if (isSigned!T && !is(T == enum)) {
57 | return JSONValue(val);
58 | }
59 |
60 | /// Serialize a signed integer.
61 | unittest {
62 | auto j3 = toJSON(41);
63 | assert(4.toJSON == JSONValue(4));
64 | assert(4L.toJSON == JSONValue(4L));
65 | }
66 |
67 | /// convert an unsigned integer to a JSONValue
68 | JSONValue toJSON(T : ulong)(T val) if (isUnsigned!T && !is(T == enum)) {
69 | return JSONValue(val);
70 | }
71 |
72 | /// Serialize an unsigned integer.
73 | unittest {
74 | assert(41u.toJSON == JSONValue(41u));
75 | }
76 |
77 | /// convert an enum name to a JSONValue
78 | JSONValue toJSON(T)(T val) if (is(T == enum)) {
79 | JSONValue json;
80 | json.str = to!string(val);
81 | return json;
82 | }
83 |
84 | /// Enums are serialized by name.
85 | unittest {
86 | enum Category { one, two }
87 |
88 | assert(Category.one.toJSON.str == "one");
89 | assert(Category.two.toJSON.str == "two");
90 | }
91 |
92 | /// convert a homogenous array into a JSONValue array
93 | JSONValue toJSON(T)(T args) if (isArray!T && !isSomeString!T) {
94 | static if (isDynamicArray!T) {
95 | if (args is null) { return JSONValue(null); }
96 | }
97 | JSONValue[] jsonVals;
98 | foreach(arg ; args) {
99 | jsonVals ~= toJSON(arg);
100 | }
101 | JSONValue json;
102 | json.array = jsonVals;
103 | return json;
104 | }
105 |
106 | /// Serialize a homogenous array.
107 | unittest {
108 | auto json = [1, 2, 3].toJSON;
109 | assert(json.type == JSONType.array);
110 | assert(json.array[0].integer == 1);
111 | assert(json.array[1].integer == 2);
112 | assert(json.array[2].integer == 3);
113 | }
114 |
115 | /// convert a set of heterogenous values into a JSONValue array
116 | JSONValue toJSON(T...)(T args) {
117 | JSONValue[] jsonVals;
118 | foreach(arg ; args) {
119 | jsonVals ~= toJSON(arg);
120 | }
121 | JSONValue json;
122 | json.array = jsonVals;
123 | return json;
124 | }
125 |
126 | /// Serialize a heterogenous array.
127 | unittest {
128 | auto json = toJSON(1, "hi", 0.4);
129 | assert(json.type == JSONType.array);
130 | assert(json.array[0].integer == 1);
131 | assert(json.array[1].str == "hi");
132 | assert(json.array[2].floating == 0.4);
133 | }
134 |
135 | /// convert a associative array into a JSONValue object
136 | JSONValue toJSON(T)(T map) if (isAssociativeArray!T) {
137 | assert(is(KeyType!T : string), "toJSON requires string keys for associative array");
138 | if (map is null) { return JSONValue(null); }
139 | JSONValue[string] obj;
140 | foreach(key, val ; map) {
141 | obj[key] = toJSON(val);
142 | }
143 | JSONValue json;
144 | json.object = obj;
145 | return json;
146 | }
147 |
148 | /// Serialize an associative array.
149 | unittest {
150 | auto json = ["a" : 1, "b" : 2, "c" : 3].toJSON;
151 | assert(json.type == JSONType.object);
152 | assert(json.object["a"].integer == 1);
153 | assert(json.object["b"].integer == 2);
154 | assert(json.object["c"].integer == 3);
155 | }
156 |
157 | /// Convert a user-defined type to json.
158 | /// See `jsonizer.jsonize` for info on how to mark your own types for serialization.
159 | JSONValue toJSON(T)(T obj) if (!isBuiltinType!T) {
160 | static if (is (T == class))
161 | if (obj is null) { return JSONValue(null); }
162 |
163 | JSONValue[string] keyValPairs;
164 |
165 | foreach(member ; T._membersWithUDA!jsonize) {
166 | enum key = jsonKey!(T, member);
167 |
168 | auto output = JsonizeOut.unspecified;
169 | foreach (attr ; T._getUDAs!(member, jsonize))
170 | if (attr.perform_out != JsonizeOut.unspecified)
171 | output = attr.perform_out;
172 |
173 | if (output == JsonizeOut.no) continue;
174 |
175 | auto val = obj._readMember!member;
176 | if (output == JsonizeOut.opt && isInitial(val)) continue;
177 |
178 | keyValPairs[key] = toJSON(val);
179 | }
180 |
181 | std.json.JSONValue json;
182 | json.object = keyValPairs;
183 | return json;
184 | }
185 |
186 | /// Serialize an instance of a user-defined type to a json object.
187 | unittest {
188 | import jsonizer.jsonize;
189 |
190 | static struct Foo {
191 | mixin JsonizeMe;
192 | @jsonize int i;
193 | @jsonize string[] a;
194 | }
195 |
196 | auto foo = Foo(12, [ "a", "b" ]);
197 | assert(foo.toJSON() == `{"i": 12, "a": [ "a", "b" ]}`.parseJSON);
198 | }
199 |
200 | /// Whether to nicely format json string.
201 | alias PrettyJson = Flag!"PrettyJson";
202 |
203 | /// Convert an instance of some type `T` directly into a json-formatted string.
204 | /// Params:
205 | /// T = type of object to convert
206 | /// obj = object to convert to sjon
207 | /// pretty = whether to prettify string output
208 | string toJSONString(T)(T obj, PrettyJson pretty = PrettyJson.yes) {
209 | auto json = obj.toJSON!T;
210 | return pretty ? json.toPrettyString : json.toString;
211 | }
212 |
213 | unittest {
214 | assert([1, 2, 3].toJSONString(PrettyJson.no) == "[1,2,3]");
215 | assert([1, 2, 3].toJSONString(PrettyJson.yes) == "[\n 1,\n 2,\n 3\n]");
216 | }
217 |
218 | /// Write a jsonizeable object to a file.
219 | /// Params:
220 | /// path = filesystem path to write json to
221 | /// obj = object to convert to json and write to path
222 | void writeJSON(T)(string path, T obj) {
223 | auto json = toJSON!T(obj);
224 | path.write(json.toPrettyString);
225 | }
226 |
227 | unittest {
228 | import std.path : buildPath;
229 | import std.uuid : randomUUID;
230 |
231 | auto dir = buildPath(tempDir(), "jsonizer_writejson_test");
232 | mkdirRecurse(dir);
233 | auto file = buildPath(dir, randomUUID().toString);
234 |
235 | file.writeJSON([1, 2, 3]);
236 |
237 | auto json = file.readText.parseJSON;
238 | assert(json.array[0].integer == 1);
239 | assert(json.array[1].integer == 2);
240 | assert(json.array[2].integer == 3);
241 | }
242 |
243 | // True if `val` is equal to the initial value of that type
244 | bool isInitial(T)(T val)
245 | {
246 | static if (is(typeof(val == T.init)))
247 | return val == T.init;
248 | else static if (is(typeof(val is T.init)))
249 | return val is T.init;
250 | else static assert(0, "isInitial can't handle " ~ T.stringof);
251 | }
252 |
--------------------------------------------------------------------------------
/tests/classes.d:
--------------------------------------------------------------------------------
1 | module tests.classes;
2 |
3 | import tests.types;
4 | import jsonizer;
5 |
6 | // assert that an object can be serialized to JSON and then reconstructed
7 | void runTest(T)(T obj) {
8 | // check JSONValue serialization
9 | assert(obj.toJSON.fromJSON!T == obj);
10 | // check JSON string serialization
11 | assert(obj.toJSONString.fromJSONString!T == obj);
12 | }
13 |
14 | /++
15 | unittest {
16 | auto obj = new OuterClass;
17 | obj.outerVal = 5;
18 | obj.inner = obj.new InnerClass;
19 | obj.inner.innerVal = 10;
20 | runTest(obj);
21 | }
22 |
23 | unittest {
24 | auto obj = new OuterClassCtor;
25 | obj.outerVal = 5;
26 | obj.inner = obj.new InnerClass(10);
27 | runTest(obj);
28 | }
29 |
30 | unittest {
31 | assert("null".fromJSONString!SimpleClass is null);
32 | }
33 | ++/
34 |
--------------------------------------------------------------------------------
/tests/structs.d:
--------------------------------------------------------------------------------
1 | import std.json;
2 |
3 | import tests.types;
4 | import jsonizer.fromjson;
5 | import jsonizer.tojson;
6 |
7 | // assert that an object can be serialized to JSON and then reconstructed
8 | void runTest(T, Params ...)(Params params) {
9 | T obj = T(params);
10 | // check JSONValue serialization
11 | assert(obj.toJSON.fromJSON!T == obj);
12 | // check JSON string serialization
13 | assert(obj.toJSONString.fromJSONString!T == obj);
14 | }
15 |
16 | // helper to make an empty instance of an array type T
17 | template emptyArray(T) {
18 | enum emptyArray = cast(T) [];
19 | }
20 |
21 | auto staticArray(T, Params ...)(Params params) {
22 | return cast(T[Params.length]) [params];
23 | }
24 |
25 | /++
26 | unittest {
27 | runTest!PrimitiveStruct(1, 2UL, true, 0.4f, 0.8, "wat?"); // general case
28 | runTest!PrimitiveStruct(0, 40, false, 0.4f, 22.8, ""); // empty string
29 | runTest!PrimitiveStruct(0, 40, false, 0.4f, 22.8, null); // null string
30 | // NaN and Inf are currently not handled by std.json
31 | //runTest!PrimitiveStruct(0, 40, false, float.nan, 22.8, null);
32 | }
33 |
34 | unittest {
35 | runTest!PrivateFieldsStruct(1, 2UL, true, 0.4f, 0.8, "wat?");
36 | }
37 |
38 | unittest {
39 | runTest!PropertyStruct(4, false, -2.7f, "asdf");
40 | }
41 |
42 | unittest {
43 | runTest!ArrayStruct([1, 2, 3], ["a", "b", "c"]); // populated arrays
44 | runTest!ArrayStruct([1, 2, 3], ["a", null, "c"]); // null in array
45 | runTest!ArrayStruct(emptyArray!(int[]), emptyArray!(string[])); // empty arrays
46 | runTest!ArrayStruct(null, null); // null arrays
47 | }
48 |
49 | unittest {
50 | runTest!NestedArrayStruct([[1, 2], [3, 4]], [["a", "b"], ["c", "d"]]); // nested arrays
51 | runTest!NestedArrayStruct([null, [3, 4]], cast(string[][]) [null]); // null entries
52 | runTest!NestedArrayStruct(emptyArray!(int[][]), emptyArray!(string[][])); // empty arrays
53 | runTest!NestedArrayStruct(null, null); // null arrays
54 | }
55 |
56 | unittest {
57 | runTest!StaticArrayStruct(staticArray!int(1, 2, 3), staticArray!string("a", "b"));
58 | }
59 |
60 | unittest {
61 | runTest!NestedStruct(5, "ra", 4.2, [2, 3]);
62 | }
63 |
64 | unittest {
65 | auto json = q{{ "inner": { "i": 1 } }};
66 | auto nested = json.fromJSONString!Nested2;
67 | assert(nested.inner.i == 1);
68 | }
69 |
70 | unittest {
71 | runTest!AliasedTypeStruct([2, 3]);
72 | }
73 |
74 | unittest {
75 | runTest!CustomCtorStruct(1, 4.2f);
76 | }
77 |
78 | unittest {
79 | runTest!(GenericStruct!int)(5);
80 | runTest!(GenericStruct!string)("s");
81 | runTest!(GenericStruct!PrimitiveStruct)(PrimitiveStruct(1, 2UL, true, 0.4f, 0.8, "wat?"));
82 | }
83 |
84 | unittest {
85 | auto jstr = "5";
86 | assert(jstr.fromJSONString!IntStruct == IntStruct(5));
87 | }
88 |
89 | unittest {
90 | assert(`{"i": 5, "j": 7}`.fromJSONString!JSONValueStruct ==
91 | JSONValueStruct(5, JSONValue(7)));
92 | assert(`{"i": 5, "j": "hi"}`.fromJSONString!JSONValueStruct ==
93 | JSONValueStruct(5, JSONValue("hi")));
94 | }
95 | ++/
96 |
--------------------------------------------------------------------------------
/tests/types.d:
--------------------------------------------------------------------------------
1 | /// Types defined for unit testing.
2 | module tests.types;
3 |
4 | import std.json;
5 | import jsonizer.jsonize;
6 |
7 | version (unittest) {
8 | }
9 | else {
10 | static assert(0, "Do not include tests dir unless in unit-test mode!");
11 | }
12 |
13 | /++
14 | struct PrimitiveStruct {
15 | mixin JsonizeMe;
16 |
17 | @jsonize {
18 | int i;
19 | ulong l;
20 | bool b;
21 | float f;
22 | double d;
23 | string s;
24 | }
25 | }
26 |
27 | struct PrivateFieldsStruct {
28 | mixin JsonizeMe;
29 |
30 | @jsonize private {
31 | int i;
32 | ulong l;
33 | bool b;
34 | float f;
35 | double d;
36 | string s;
37 | }
38 | }
39 |
40 | struct PropertyStruct {
41 | mixin JsonizeMe;
42 |
43 | @jsonize @property {
44 | // getters
45 | auto i() { return _i; }
46 | auto b() { return _b; }
47 | auto f() { return _f; }
48 | auto s() { return _s; }
49 |
50 | // setters
51 | void i(int val) { _i = val; }
52 | void b(bool val) { _b = val; }
53 | void f(float val) { _f = val; }
54 | void s(string val) { _s = val; }
55 | }
56 |
57 | private:
58 | int _i;
59 | bool _b;
60 | float _f;
61 | string _s;
62 | }
63 |
64 | struct ArrayStruct {
65 | mixin JsonizeMe;
66 |
67 | @jsonize {
68 | int[] i;
69 | string[] s;
70 | }
71 | }
72 |
73 | struct NestedArrayStruct {
74 | mixin JsonizeMe;
75 |
76 | @jsonize {
77 | int[][] i;
78 | string[][] s;
79 | }
80 | }
81 |
82 | struct StaticArrayStruct {
83 | mixin JsonizeMe;
84 |
85 | @jsonize {
86 | int[3] i;
87 | string[2] s;
88 | }
89 |
90 | bool opEquals(StaticArrayStruct other) {
91 | return i == other.i && s == other.s;
92 | }
93 | }
94 |
95 | struct NestedStruct {
96 | mixin JsonizeMe;
97 | @jsonize {
98 | int i;
99 | string s;
100 | Inner inner;
101 | }
102 |
103 | private struct Inner {
104 | mixin JsonizeMe;
105 | @jsonize {
106 | double d;
107 | int[] a;
108 | }
109 | }
110 |
111 | this(int i, string s, double d, int[] a) {
112 | this.i = i;
113 | this.s = s;
114 | inner = Inner(d, a);
115 | }
116 | }
117 |
118 | struct Nested2 {
119 | mixin JsonizeMe;
120 | @jsonize Inner inner;
121 |
122 | private struct Inner {
123 | mixin JsonizeMe;
124 | @jsonize int i;
125 | }
126 | }
127 |
128 | struct AliasedTypeStruct {
129 | mixin JsonizeMe;
130 | alias Ints = int[];
131 | @jsonize Ints i;
132 | }
133 |
134 | struct CustomCtorStruct {
135 | mixin JsonizeMe;
136 |
137 | @disable this();
138 |
139 | @jsonize {
140 | int i;
141 | float f;
142 |
143 | this(int i, float f) {
144 | this.i = i;
145 | this.f = f;
146 | }
147 | }
148 | }
149 |
150 | struct JSONValueStruct {
151 | mixin JsonizeMe;
152 |
153 | @jsonize {
154 | int i;
155 | JSONValue j;
156 | }
157 | }
158 |
159 | class SimpleClass {
160 | mixin JsonizeMe;
161 | @jsonize {
162 | int i;
163 | string s;
164 | }
165 | }
166 |
167 | class OuterClass {
168 | mixin JsonizeMe;
169 |
170 | @jsonize {
171 | int outerVal;
172 | InnerClass inner;
173 | }
174 |
175 | class InnerClass {
176 | mixin JsonizeMe;
177 | @jsonize int innerVal;
178 |
179 | override bool opEquals(Object obj) {
180 | auto other = cast(InnerClass) obj;
181 | return other !is null && this.innerVal == other.innerVal;
182 | }
183 | }
184 |
185 | override bool opEquals(Object obj) {
186 | auto other = cast(OuterClass) obj;
187 | return other !is null && this.outerVal == other.outerVal;
188 | }
189 | }
190 |
191 | class OuterClassCtor {
192 | mixin JsonizeMe;
193 |
194 | @jsonize {
195 | int outerVal;
196 | InnerClass inner;
197 | }
198 |
199 | class InnerClass {
200 | mixin JsonizeMe;
201 | @jsonize int i;
202 |
203 | @jsonize this(int i) { this.i = i; }
204 | }
205 |
206 | override bool opEquals(Object obj) {
207 | auto other = cast(OuterClassCtor) obj;
208 | return other !is null && outerVal == other.outerVal && inner.i == other.inner.i;
209 | }
210 | }
211 |
212 | struct GenericStruct(T) {
213 | mixin JsonizeMe;
214 |
215 | @jsonize T val;
216 | }
217 |
218 | struct IntStruct {
219 | mixin JsonizeMe;
220 | int i;
221 | @jsonize this(int i) { this.i = i; }
222 | }
223 | ++/
224 |
--------------------------------------------------------------------------------