1 /// serialize and deserialize between JSONValues and other D types
2 module jsonizer;
3 
4 import std.json;
5 import std.file;
6 import std.conv;
7 import std.range;
8 import std.traits;
9 import std.string;
10 import std.algorithm;
11 import std.exception;
12 import std.typetuple;
13 import std.typecons : staticIota;
14 
15 /// json member used to map a json object to a D type  
16 enum jsonizeClassKeyword = "class";
17 
18 /// use @jsonize to mark members to be (de)serialized from/to json
19 /// use @jsonize to mark a single contructor to use when creating an object using extract
20 /// use @jsonize("name") to make a member use the json key "name"
21 struct jsonize {
22   string key;
23 }
24 
25 // Primitive Type Conversions -----------------------------------------------------------
26 /// convert a bool to a JSONValue
27 JSONValue toJSON(T : bool)(T val) {
28   return JSONValue(val);
29 }
30 
31 /// convert a string to a JSONValue
32 JSONValue toJSON(T : string)(T val) {
33   return JSONValue(val);
34 }
35 
36 /// convert a floating point value to a JSONValue
37 JSONValue toJSON(T : real)(T val) if (!is(T == enum)) {
38   return JSONValue(val);
39 }
40 
41 /// convert a signed integer to a JSONValue
42 JSONValue toJSON(T : long)(T val) if (isSigned!T && !is(T == enum)) {
43   return JSONValue(val);
44 }
45 
46 /// convert an unsigned integer to a JSONValue
47 JSONValue toJSON(T : ulong)(T val) if (isUnsigned!T && !is(T == enum)) {
48   return JSONValue(val);
49 }
50 
51 /// convert an enum name to a JSONValue
52 JSONValue toJSON(T)(T val) if (is(T == enum)) {
53   JSONValue json;
54   json.str = to!string(val);
55   return json;
56 }
57 
58 /// convert a homogenous array into a JSONValue array
59 JSONValue toJSON(T)(T args) if (isArray!T && !isSomeString!T) {
60   static if (isDynamicArray!T) {
61     if (args is null) { return JSONValue(null); }
62   }
63   JSONValue[] jsonVals;
64   foreach(arg ; args) {
65     jsonVals ~= toJSON(arg);
66   }
67   JSONValue json;
68   json.array = jsonVals;
69   return json;
70 }
71 
72 /// convert a set of heterogenous values into a JSONValue array
73 JSONValue toJSON(T...)(T args) {
74   JSONValue[] jsonVals;
75   foreach(arg ; args) {
76     jsonVals ~= toJSON(arg);
77   }
78   JSONValue json;
79   json.array = jsonVals;
80   return json;
81 }
82 
83 /// convert a associative array into a JSONValue object
84 JSONValue toJSON(T)(T map) if (isAssociativeArray!T) {
85   assert(is(KeyType!T : string), "toJSON requires string keys for associative array");
86   if (map is null) { return JSONValue(null); }
87   JSONValue[string] obj;
88   foreach(key, val ; map) {
89     obj[key] = toJSON(val);
90   }
91   JSONValue json;
92   json.object = obj;
93   return json;
94 }
95 
96 JSONValue toJSON(T)(T obj) if (!isBuiltinType!T) {
97   static if (is (T == class)) {
98     if (obj is null) { return JSONValue(null); }
99   }
100   return obj.convertToJSON();
101 }
102 
103 // Extraction ------------------------------------------------------------------
104 private void enforceJsonType(T)(JSONValue json, JSON_TYPE[] expected ...) {
105   enum fmt = "extract!%s expected json type to be one of %s but got json type %s. json input: %s";
106   enforce(expected.canFind(json.type), format(fmt, typeid(T), expected, json.type, json));
107 }
108 
109 /// extract a boolean from a json value
110 T extract(T : bool)(JSONValue json) {
111   if (json.type == JSON_TYPE.TRUE) {
112     return true;
113   }
114   else if (json.type == JSON_TYPE.FALSE) {
115     return false;
116   }
117   else {
118     enforce(0, format("tried to extract bool from json of type %s", json.type));
119   }
120 }
121 
122 /// extract a string type from a json value
123 T extract(T : string)(JSONValue json) {
124   if (json.type == JSON_TYPE.NULL) { return null; }
125   enforceJsonType!T(json, JSON_TYPE.STRING);
126   return cast(T) json.str;
127 }
128 
129 /// extract a numeric type from a json value
130 T extract(T : real)(JSONValue json) if (!is(T == enum)) {
131   switch(json.type) {
132     case JSON_TYPE.FLOAT:
133       return cast(T) json.floating;
134     case JSON_TYPE.INTEGER:
135       return cast(T) json.integer;
136     case JSON_TYPE.UINTEGER:
137       return cast(T) json.uinteger;
138     case JSON_TYPE.STRING:
139       enforce(json.str.isNumeric, format("tried to extract %s from json string %s", T.stringof, json.str));
140       return to!T(json.str); // try to parse string as int
141     default:
142       enforce(0, format("tried to extract %s from json of type %s", T.stringof, json.type));
143   }
144   assert(0, "should not be reacheable");
145 }
146 
147 /// extract an enumerated type from a json value
148 T extract(T)(JSONValue json) if (is(T == enum)) {
149   enforceJsonType!T(json, JSON_TYPE.STRING);
150   return to!T(json.str);
151 }
152 
153 /// extract an array from a JSONValue
154 T extract(T)(JSONValue json) if (isArray!T && !isSomeString!(T)) {
155   if (json.type == JSON_TYPE.NULL) { return T.init; }
156   enforceJsonType!T(json, JSON_TYPE.ARRAY);
157   alias ElementType = ForeachType!T;
158   T vals;
159   foreach(idx, val ; json.array) {
160     static if (isStaticArray!T) {
161       vals[idx] = val.extract!ElementType;
162     }
163     else {
164       vals ~= val.extract!ElementType;
165     }
166   }
167   return vals;
168 }
169 
170 /// extract an associative array from a JSONValue
171 T extract(T)(JSONValue json) if (isAssociativeArray!T) {
172   assert(is(KeyType!T : string), "toJSON requires string keys for associative array");
173   if (json.type == JSON_TYPE.NULL) { return null; }
174   enforceJsonType!T(json, JSON_TYPE.OBJECT);
175   alias ValType = ValueType!T;
176   T map;
177   foreach(key, val ; json.object) {
178     map[key] = extract!ValType(val);
179   }
180   return map;
181 }
182 
183 /// extract a value from a json object by its key
184 T extract(T)(JSONValue json, string key) {
185   enforceJsonType!T(json, JSON_TYPE.OBJECT);
186   enforce(key in json.object, "tried to extract non-existent key " ~ key ~ " from JSONValue");
187   return extract!T(json.object[key]);
188 }
189 
190 /// extract a value from a json object by its key, return defaultVal if key not found
191 T extract(T)(JSONValue json, string key, T defaultVal) {
192   enforceJsonType!T(json, JSON_TYPE.OBJECT);
193   return (key in json.object) ? extract!T(json.object[key]) : defaultVal;
194 }
195 
196 /// extract a user-defined class or struct from a JSONValue
197 T extract(T)(JSONValue json) if (!isBuiltinType!T) {
198   static if (is(T == class)) {
199     if (json.type == JSON_TYPE.NULL) { return null; }
200   }
201   enforceJsonType!T(json, JSON_TYPE.OBJECT);
202 
203   static if (is(typeof(null) : T)) {
204     // look for class keyword in json
205     auto className = json.extract!string(jsonizeClassKeyword, null);
206     // try creating an instance with Object.factory 
207     if (className !is null) {
208       auto obj = Object.factory(className);
209       assert(obj !is null, "failed to Object.factory " ~ className);
210       auto instance = cast(T) obj;
211       assert(instance !is null, "failed to cast " ~ className ~ " to " ~ T.stringof);
212       instance.populateFromJSON(json);
213       return instance;
214     }
215   }
216 
217   // next, try to find a contructor marked with @jsonize and call that
218   static if (__traits(hasMember, T, "__ctor")) {
219     alias Overloads = TypeTuple!(__traits(getOverloads, T, "__ctor"));
220     foreach(overload ; Overloads) {
221       if (staticIndexOf!(jsonize, __traits(getAttributes, overload)) >= 0 &&
222           canSatisfyCtor!overload(json)) {
223         return invokeCustomJsonCtor!(T, overload)(json);
224       }
225     }
226   }
227 
228   // if no @jsonized ctor, try to use a default ctor and populate the fields
229   static if(is(T == struct) || is(typeof(new T) == T)) { // can object be default constructed?
230     return invokeDefaultCtor!(T)(json);
231   }
232   assert(0, T.stringof ~ " must have a no-args constructor to support extract");
233 }
234 
235 /// return true if keys can satisfy parameter names
236 private bool canSatisfyCtor(alias Ctor)(JSONValue json) {
237   auto obj = json.object;
238   alias Params   = ParameterIdentifierTuple!Ctor;
239   alias Types    = ParameterTypeTuple!Ctor;
240   alias Defaults = ParameterDefaultValueTuple!Ctor;
241   foreach(i ; staticIota!(0, Params.length)) {
242     if (Params[i] !in obj && typeid(Defaults[i]) == typeid(void)) {
243       return false; // param had no default value and was not specified
244     }
245   }
246   return true;
247 }
248 
249 private T invokeCustomJsonCtor(T, alias Ctor)(JSONValue json) {
250   enum params    = ParameterIdentifierTuple!(Ctor);
251   alias defaults = ParameterDefaultValueTuple!(Ctor);
252   alias Types    = ParameterTypeTuple!(Ctor);
253   Tuple!(Types) args;
254   foreach(i ; staticIota!(0, params.length)) {
255     enum paramName = params[i];
256     if (paramName in json.object) {
257       args[i] = json.object[paramName].extract!(Types[i]);
258     }
259     else { // no value specified in json
260       static if (is(defaults[i] == void)) {
261         enforce(0, "parameter " ~ paramName ~ " has no default value and was not specified");
262       }
263       else {
264         args[i] = defaults[i];
265       }
266     }
267   }
268   static if (is(T == class)) {
269     return new T(args.expand);
270   }
271   else {
272     return T(args.expand);
273   }
274 }
275 
276 private T invokeDefaultCtor(T)(JSONValue json) {
277   T obj;
278   static if (is(T == struct)) {
279     obj = T.init;
280   }
281   else {
282     obj = new T;
283   }
284   obj.populateFromJSON(json);
285   return obj;
286 }
287 
288 bool hasCustomJsonCtor(T)() {
289   static if (__traits(hasMember, T, "__ctor")) {
290     alias Overloads = TypeTuple!(__traits(getOverloads, T, "__ctor"));
291     foreach(overload ; Overloads) {
292       static if (staticIndexOf!(jsonize, __traits(getAttributes, overload)) >= 0) {
293         return true;
294       }
295     }
296   }
297   return false;
298 }
299 
300 string jsonizeKey(alias obj, string memberName)() {
301   foreach(attr ; __traits(getAttributes, mixin("obj." ~ memberName))) {
302     static if (is(attr == jsonize)) { // @jsonize someMember;
303       return memberName;          // use member name as-is
304     }
305     else static if (is(typeof(attr) == jsonize)) { // @jsonize("someKey") someMember;
306       return attr.key;
307     }
308   }
309   return null;
310 } 
311 
312 mixin template JsonizeMe() {
313   import std.json      : JSONValue;
314   import std.typetuple : Erase;
315   import std.traits    : BaseClassesTuple;
316 
317   alias T = typeof(this);
318   static if (is(T == class) && 
319       __traits(hasMember, BaseClassesTuple!T[0], "populateFromJSON")) 
320   {
321     override void populateFromJSON(JSONValue json) {
322       static if (!hasCustomJsonCtor!T) {
323         auto keyValPairs = json.object;
324         // check if each member is actually a member and is marked with the @jsonize attribute
325         foreach(member ; Erase!("__ctor", __traits(allMembers, T))) {
326           enum key = jsonizeKey!(this, member);              // find @jsonize, deduce member key
327           static if (key !is null) {
328             alias MemberType = typeof(mixin(member));        // deduce member type
329             auto val = extract!MemberType(keyValPairs[key]); // extract value from json
330             mixin(member ~ "= val;");                        // assign value to member
331           }
332         }
333       }
334     }
335 
336     override JSONValue convertToJSON() {
337       JSONValue[string] keyValPairs;
338       // look for members marked with @jsonize, ignore __ctor
339       foreach(member ; Erase!("__ctor", __traits(allMembers, T))) {
340         enum key = jsonizeKey!(this, member); // find @jsonize, deduce member key
341         static if(key !is null) {
342           auto val = mixin(member);           // get the member's value
343           keyValPairs[key] = toJSON(val);     // add the pair <memberKey> : <memberValue>
344         }
345       }
346       // construct the json object
347       JSONValue json;
348       json.object = keyValPairs;
349       return json;
350     }
351   }
352   else {
353     void populateFromJSON(JSONValue json) {
354       static if (!hasCustomJsonCtor!T) {
355         auto keyValPairs = json.object;
356         // check if each member is actually a member and is marked with the @jsonize attribute
357         foreach(member ; Erase!("__ctor", __traits(allMembers, T))) {
358           enum key = jsonizeKey!(this, member);              // find @jsonize, deduce member key
359           static if (key !is null) {
360             alias MemberType = typeof(mixin(member));        // deduce member type
361             auto val = extract!MemberType(keyValPairs[key]); // extract value from json
362             mixin(member ~ "= val;");                        // assign value to member
363           }
364         }
365       }
366     }
367 
368     JSONValue convertToJSON() {
369       JSONValue[string] keyValPairs;
370       // look for members marked with @jsonize, ignore __ctor
371       foreach(member ; Erase!("__ctor", __traits(allMembers, T))) {
372         enum key = jsonizeKey!(this, member); // find @jsonize, deduce member key
373         static if(key !is null) {
374           auto val = mixin(member);           // get the member's value
375           keyValPairs[key] = toJSON(val);     // add the pair <memberKey> : <memberValue>
376         }
377       }
378       // construct the json object
379       JSONValue json;
380       json.object = keyValPairs;
381       return json;
382     }
383   }
384 }
385 
386 // Reading/Writing JSON Files
387 /// read a json-constructable object from a file
388 T readJSON(T)(string file) {
389   auto json = parseJSON(readText(file));
390   return extract!T(json);
391 }
392 
393 /// shortcut to read file directly into JSONValue
394 auto readJSON(string file) {
395   return parseJSON(readText(file));
396 }
397 
398 /// write a jsonizeable object to a file
399 void writeJSON(T)(T obj, string file) {
400   auto json = toJSON!T(obj);
401   file.write(json.toPrettyString);
402 }
403 
404 /// json conversion of primitive types
405 unittest {
406   import std.math : approxEqual;
407   enum Category { one, two }
408 
409   auto j1 = toJSON("bork");
410   assert(j1.type == JSON_TYPE.STRING && j1.str == "bork");
411   assert(extract!string(j1) == "bork");
412 
413   auto j2 = toJSON(4.1);
414   assert(j2.type == JSON_TYPE.FLOAT && j2.floating.approxEqual(4.1));
415   assert(extract!float(j2).approxEqual(4.1));
416   assert(extract!double(j2).approxEqual(4.1));
417   assert(extract!real(j2).approxEqual(4.1));
418 
419   auto j3 = toJSON(41);
420   assert(j3.type == JSON_TYPE.INTEGER && j3.integer == 41);
421   assert(extract!int(j3) == 41);
422   assert(extract!long(j3) == 41);
423 
424   auto j4 = toJSON(41u);
425   assert(j4.type == JSON_TYPE.UINTEGER && j4.uinteger == 41u);
426   assert(extract!uint(j4) == 41u);
427   assert(extract!ulong(j4) == 41u);
428 
429   auto jenum = toJSON!Category(Category.one);
430   assert(jenum.type == JSON_TYPE.STRING);
431   assert(jenum.extract!Category == Category.one);
432 
433   // homogenous json array
434   auto j5 = toJSON([9, 8, 7, 6]);
435   assert(j5.array[0].integer == 9);
436   assert(j5.array[1].integer == 8);
437   assert(j5.array[2].integer == 7);
438   assert(j5.array[3].integer == 6);
439   assert(j5.type == JSON_TYPE.ARRAY);
440   assert(extract!(int[])(j5) == [9, 8, 7, 6]);
441 
442   // heterogenous json array
443   auto j6 = toJSON("sammich", 1.5, 2, 3u);
444   assert(j6.array[0].str == "sammich");
445   assert(j6.array[1].floating.approxEqual(1.5));
446   assert(j6.array[2].integer == 2);
447   assert(j6.array[3].uinteger == 3u);
448 
449   // associative array
450   int[string] aa = ["a" : 1, "b" : 2, "c" : 3];
451   auto j7 = toJSON(aa);
452   assert(j7.type == JSON_TYPE.OBJECT);
453   assert(j7.object["a"].integer == 1);
454   assert(j7.object["b"].integer == 2);
455   assert(j7.object["c"].integer == 3);
456   assert(extract!(int[string])(j7) == aa);
457   assert(j7.extract!int("b") == 2);
458 }
459 
460 /// object serialization -- fields only
461 unittest {
462   import std.math : approxEqual;
463 
464   static class Fields {
465     this() { } // class must have a no-args ctor
466 
467     this(int iVal, float fVal, string sVal, int[] aVal, string noJson) {
468       i = iVal;
469       f = fVal;
470       s = sVal;
471       a = aVal;
472       dontJsonMe = noJson;
473     }
474 
475     mixin JsonizeMe;
476 
477     @jsonize { // fields to jsonize -- test different access levels
478       public int i;
479       protected float f;
480       public int[] a;
481       private string s;
482     }
483     string dontJsonMe;
484 
485     override bool opEquals(Object o) {
486       auto other = cast(Fields) o;
487       return i == other.i && s == other.s && a == other.a && f.approxEqual(other.f);
488     }
489   }
490 
491   auto obj = new Fields(1, 4.2, "tally ho!", [9, 8, 7, 6], "blarg");
492   auto json = toJSON!Fields(obj);
493 
494   assert(json.object["i"].integer == 1);
495   assert(json.object["f"].floating.approxEqual(4.2));
496   assert(json.object["s"].str == "tally ho!");
497   assert(json.object["a"].array[0].integer == 9);
498   assert("dontJsonMe" !in json.object);
499 
500   // reconstruct from json
501   auto r = extract!Fields(json);
502   assert(r.i == 1);
503   assert(r.f.approxEqual(4.2));
504   assert(r.s == "tally ho!");
505   assert(r.a == [9, 8, 7, 6]);
506   assert(r.dontJsonMe is null);
507 
508   // array of objects
509   auto a = [
510     new Fields(1, 4.2, "tally ho!", [9, 8, 7, 6], "blarg"),
511         new Fields(7, 42.2, "yea merrily", [1, 4, 6, 4], "asparagus")
512   ];
513 
514   // serialize array of user objects to json
515   auto jsonArray = toJSON!(Fields[])(a);
516   // reconstruct from json
517   assert(extract!(Fields[])(jsonArray) == a);
518 }
519 
520 /// object serialization with properties
521 unittest {
522   import std.math : approxEqual;
523 
524   static class Props {
525     this() { } // class must have a no-args ctor
526 
527     this(int iVal, float fVal, string sVal, string noJson) {
528       _i = iVal;
529       _f = fVal;
530       _s = sVal;
531       _dontJsonMe = noJson;
532     }
533 
534     mixin JsonizeMe;
535 
536     @property {
537       // jsonize ref property accessing private field
538       @jsonize ref int i() { return _i; }
539       // jsonize property with non-trivial get/set methods
540       @jsonize float f() { return _f - 3; } // the jsonized value will equal _f - 3
541       float f(float val) { return _f = val + 5; } // 5 will be added to _f when retrieving from json
542       // don't jsonize these properties
543       ref string s() { return _s; }
544       ref string dontJsonMe() { return _dontJsonMe; }
545     }
546 
547     private:
548     int _i;
549     float _f;
550     @jsonize string _s;
551     string _dontJsonMe;
552   }
553 
554   auto obj = new Props(1, 4.2, "tally ho!", "blarg");
555   auto json = toJSON(obj);
556 
557   assert(json.object["i"].integer == 1);
558   assert(json.object["f"].floating.approxEqual(4.2 - 3.0)); // property should have subtracted 3 on retrieval
559   assert(json.object["_s"].str == "tally ho!");
560   assert("dontJsonMe" !in json.object);
561 
562   auto r = extract!Props(json);
563   assert(r.i == 1);
564   assert(r._f.approxEqual(4.2 - 3.0 + 5.0)); // property accessor should add 5
565   assert(r._s == "tally ho!");
566   assert(r.dontJsonMe is null);
567 }
568 
569 /// object serialization with custom constructor
570 unittest {
571   import std.math : approxEqual;
572 
573   static class Custom {
574     mixin JsonizeMe;
575 
576     this(int i) {
577       _i = i;
578       _s = "something";
579       _f = 10.2;
580     }
581 
582     @jsonize this(int _i, string _s, float _f = 20.2) {
583       this._i = _i;
584       this._s = _s ~ " jsonized";
585       this._f = _f;
586     }
587 
588     @jsonize this(double d) { // alternate ctor
589       _f = d.to!float;
590       _s = d.to!string;
591       _i = d.to!int;
592     }
593 
594     private:
595     @jsonize {
596       string _s;
597       float _f;
598       int _i;
599     }
600   }
601 
602   auto c = new Custom(12);
603   auto json = toJSON(c);
604   assert(json.object["_i"].integer == 12);
605   assert(json.object["_s"].str == "something");
606   assert(json.object["_f"].floating.approxEqual(10.2));
607   auto c2 = extract!Custom(json);
608   assert(c2._i == 12);
609   assert(c2._s == "something jsonized");
610   assert(c2._f.approxEqual(10.2));
611 
612   // test alternate ctor
613   json = parseJSON(`{"d" : 5}`);
614   c = json.extract!Custom;
615   assert(c._f.approxEqual(5) && c._i == 5 && c._s == "5");
616 }
617 
618 /// struct serialization
619 unittest {
620   import std.math : approxEqual;
621 
622   static struct S {
623     mixin JsonizeMe;
624 
625     @jsonize {
626       int x;
627       float f;
628       string s;
629     }
630     int dontJsonMe;
631 
632     this(int x, float f, string s, int noJson) {
633       this.x = x;
634       this.f = f;
635       this.s = s;
636       this.dontJsonMe = noJson;
637     }
638   }
639 
640   auto s = S(5, 4.2, "bogus", 7);
641   auto json = toJSON(s); // serialize a struct
642 
643   assert(json.object["x"].integer == 5);
644   assert(json.object["f"].floating.approxEqual(4.2));
645   assert(json.object["s"].str == "bogus");
646   assert("dontJsonMe" !in json.object);
647 
648   auto r = extract!S(json);
649   assert(r.x == 5);
650   assert(r.f.approxEqual(4.2));
651   assert(r.s == "bogus");
652   assert(r.dontJsonMe == int.init);
653 }
654 
655 /// json file I/O
656 unittest {
657   enum file = "test.json";
658   scope(exit) remove(file);
659 
660   static struct Data {
661     mixin JsonizeMe;
662 
663     @jsonize {
664       int x;
665       string s;
666       float f;
667     }
668   }
669 
670   // write an array of user-defined structs
671   auto array = [Data(5, "preposterous", 12.7), Data(8, "tesseract", -2.7), Data(5, "baby sloths", 102.7)];
672   writeJSON(array, file);
673   auto readBack = readJSON!(Data[])(file);
674   assert(readBack == array);
675 
676   // now try an associative array
677   auto aa = ["alpha": Data(27, "yams", 0), "gamma": Data(88, "spork", -99.999)];
678   writeJSON(aa, file);
679   auto aaReadBack = readJSON!(Data[string])(file);
680   assert(aaReadBack == aa);
681 }
682 
683 /// inheritance
684 unittest {
685   import std.math : approxEqual;
686   static class Parent {
687     mixin JsonizeMe;
688     @jsonize {
689       int x;
690       string s;
691     }
692   }
693 
694   static class Child : Parent {
695     mixin JsonizeMe;
696     @jsonize {
697       float f;
698     }
699   }
700 
701   auto c = new Child;
702   c.x = 5;
703   c.s = "hello";
704   c.f = 2.1;
705 
706   auto json = c.toJSON;
707   assert(json.extract!int("x") == 5);
708   assert(json.extract!string("s") == "hello");
709   assert(json.extract!float("f").approxEqual(2.1));
710 
711   auto child = json.extract!Child;
712   assert(child.x == 5 && child.s == "hello" && child.f.approxEqual(2.1));
713 
714   auto parent = json.extract!Parent;
715   assert(parent.x == 5 && parent.s == "hello");
716 }
717 
718 /// inheritance with  ctors
719 unittest {
720   import std.math : approxEqual;
721   static class Parent {
722     mixin JsonizeMe;
723 
724     @jsonize this(int x, string s) {
725       _x = x;
726       _s = s;
727     }
728 
729     @jsonize @property {
730       int x()    { return _x; }
731       string s() { return _s; }
732     }
733 
734     private:
735     int _x;
736     string _s;
737   }
738 
739   static class Child : Parent {
740     mixin JsonizeMe;
741 
742     @jsonize this(int x, string s, float f) {
743       super(x, s);
744       _f = f;
745     }
746 
747     @jsonize @property {
748       float f() { return _f; }
749     }
750 
751     private:
752     float _f;
753   }
754 
755   auto c = new Child(5, "hello", 2.1);
756 
757   auto json = c.toJSON;
758   assert(json.extract!int("x") == 5);
759   assert(json.extract!string("s") == "hello");
760   assert(json.extract!float("f").approxEqual(2.1));
761 
762   auto child = json.extract!Child;
763   assert(child.x == 5 && child.s == "hello" && child.f.approxEqual(2.1));
764 
765   auto parent = json.extract!Parent;
766   assert(parent.x == 5 && parent.s == "hello");
767 }
768 
769 /// renamed members
770 unittest {
771   static class Bleh {
772     mixin JsonizeMe;
773     private {
774       @jsonize("x") int _x;
775       @jsonize("s") string _s;
776     }
777   }
778 
779   auto b = new Bleh;
780   b._x = 5;
781   b._s = "blah";
782 
783   auto json = b.toJSON;
784 
785   assert(json.extract!int("x") == 5);
786   assert(json.extract!string("s") == "blah");
787 
788   auto reconstruct = json.extract!Bleh;
789   assert(reconstruct._x == b._x && reconstruct._s == b._s);
790 }
791 
792 
793 // unfortunately these test classes must be implemented outside the unittest
794 // as Object.factory (and ClassInfo.find) cannot work with nested classes
795 private {
796   class TestComponent {
797     mixin JsonizeMe;
798     @jsonize int c;
799   }
800 
801   class TestCompA : TestComponent {
802     mixin JsonizeMe;
803     @jsonize int a;
804   }
805 
806   class TestCompB : TestComponent {
807     mixin JsonizeMe;
808     @jsonize string b;
809   }
810 }
811 
812 /// type inference
813 unittest {
814   import std.traits : fullyQualifiedName;
815 
816   // need to use these because unittest is assigned weird name
817   // normally would just be "modulename.classname"
818   string classKeyA = fullyQualifiedName!TestCompA;
819   string classKeyB = fullyQualifiedName!TestCompB;
820 
821   assert(Object.factory(classKeyA) !is null && Object.factory(classKeyB) !is null, 
822       "cannot factory classes in unittest -- this is a problem with the test");
823 
824   auto data = `[
825     {
826       "class": "%s",
827       "c": 1,
828       "a": 5
829     },
830     {
831       "class": "%s",
832       "c": 2,
833       "b": "hello"
834     }
835   ]`.format(classKeyA, classKeyB).parseJSON.extract!(TestComponent[]);
836 
837   auto a = cast(TestCompA) data[0];
838   auto b = cast(TestCompB) data[1];
839 
840   assert(a !is null && a.c == 1 && a.a == 5);
841   assert(b !is null && b.c == 2 && b.b == "hello");
842 }