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 }