1 /** 2 * Enables marking user-defined types for JSON serialization. 3 * 4 * Authors: <a href="https://github.com/rcorre">rcorre</a> 5 * License: <a href="http://opensource.org/licenses/MIT">MIT</a> 6 * Copyright: Copyright © 2015, rcorre 7 * Date: 3/23/15 8 */ 9 module jsonizer.jsonize; 10 11 import jsonizer.tojson : toJSON; 12 import jsonizer.fromjson : fromJSON; 13 import jsonizer.internal.util; 14 15 public import jsonizer.internal.attribute; 16 17 /// Generate json (de)serialization methods for the type this is mixed in to. 18 /// The methods `_toJSON` and `_fromJSON` are generated. 19 /// Params: 20 /// ignoreExtra = whether to silently ignore json keys that do not map to serialized members 21 mixin template JsonizeMe(JsonizeIgnoreExtraKeys ignoreExtra = JsonizeIgnoreExtraKeys.yes) { 22 static import std.json; 23 24 // Nested mixins -- these generate private functions to perform serialization/deserialization 25 private mixin template MakeDeserializer() { 26 private void _fromJSON(std.json.JSONValue json) { 27 // scoped imports include necessary functions without avoid polluting class namespace 28 import std.traits : isNested, isAggregateType; 29 import std.algorithm : filter; 30 import jsonizer.jsonize : JsonizeIgnoreExtraKeys; 31 import jsonizer.fromjson : fromJSON, nestedFromJSON, hasCustomJsonCtor; 32 import jsonizer.exceptions : JsonizeMismatchException; 33 import jsonizer.internal.util; 34 35 alias T = typeof(this); 36 37 // TODO: look into moving this up a level and not generating _fromJSON at all. 38 static if (!hasCustomJsonCtor!T) { 39 // track fields found to detect keys that do not map to serialized fields 40 int fieldsFound = 0; 41 string[] missingKeys; 42 auto keyValPairs = json.object; 43 44 // check if each member is actually a member and is marked with the @jsonize attribute 45 foreach(member ; filteredMembers!T) { 46 // even with filtering members, need to make sure this is a valid member. 47 // Things like nested class types will make it through. 48 static if (__traits(compiles, __traits(getMember, this, member))) { 49 // find @jsonize, deduce member key 50 enum key = jsonizeKey!(__traits(getMember, this, member), member); 51 } 52 else { 53 enum key = null; // not a real member 54 } 55 56 static if (key !is null) { 57 if (key in keyValPairs) { 58 ++fieldsFound; 59 alias MemberType = typeof(mixin(member)); // deduce member type 60 // special handling for nested class types 61 static if (isAggregateType!MemberType && isNested!MemberType) { 62 auto val = nestedFromJSON!MemberType(keyValPairs[key], this); 63 } 64 else { 65 auto val = fromJSON!MemberType(keyValPairs[key]); // extract value from json 66 } 67 mixin("this." ~ member ~ "= val;"); // assign value to member 68 } 69 else { 70 static if (!isOptional!(__traits(getMember, this, member))) { 71 missingKeys ~= key; 72 } 73 } 74 } 75 } 76 77 bool extraKeyFailure = false; // should we fail due to extra keys? 78 static if (ignoreExtra == JsonizeIgnoreExtraKeys.no) { 79 extraKeyFailure = (fieldsFound != keyValPairs.keys.length); 80 } 81 82 // check for failure condition 83 // TODO: clean up with template to get all @jsonized members 84 if (extraKeyFailure || missingKeys.length > 0) { 85 string[] extraKeys; 86 foreach(jsonKey ; json.object.byKey) { 87 bool match = false; 88 foreach(member ; filteredMembers!T) { 89 static if (__traits(compiles, __traits(getMember, this, member))) { 90 enum memberKey = jsonizeKey!(__traits(getMember, this, member), member); 91 } 92 else { 93 enum memberKey = null; 94 } 95 96 if (memberKey == jsonKey) { 97 match = true; 98 break; 99 } 100 } 101 if (!match) { 102 extraKeys ~= jsonKey; 103 } 104 } 105 106 throw new JsonizeMismatchException(typeid(T), extraKeys, missingKeys); 107 } 108 } 109 } 110 } 111 112 private mixin template MakeSerializer() { 113 private std.json.JSONValue _toJSON() { 114 import jsonizer.tojson : toJSON; 115 import jsonizer.internal.util : jsonizeKey, filteredMembers; 116 117 alias T = typeof(this); 118 119 std.json.JSONValue[string] keyValPairs; 120 // look for members marked with @jsonize, ignore __ctor 121 foreach(member ; filteredMembers!T) { 122 // find @jsonize, deduce member key 123 static if (__traits(compiles, __traits(getMember, this, member))) { 124 enum key = jsonizeKey!(__traits(getMember, this, member), member); 125 } 126 else { 127 enum key = null; 128 } 129 130 static if(key !is null) { 131 auto val = mixin("this." ~ member); // get the member's value 132 keyValPairs[key] = toJSON(val); // add the pair <memberKey> : <memberValue> 133 } 134 } 135 // construct the json object 136 std.json.JSONValue json; 137 json.object = keyValPairs; 138 return json; 139 } 140 } 141 142 // generate private functions with no override specifiers 143 mixin MakeSerializer GeneratedSerializer; 144 mixin MakeDeserializer GeneratedDeserializer; 145 146 // expose the methods generated above by wrapping them in public methods. 147 // apply the overload attribute to the public methods if already implemented in base class. 148 static if (is(typeof(this) == class) && 149 __traits(hasMember, std.traits.BaseClassesTuple!(typeof(this))[0], "populateFromJSON")) 150 { 151 override void populateFromJSON(std.json.JSONValue json) { 152 GeneratedDeserializer._fromJSON(json); 153 } 154 155 override std.json.JSONValue convertToJSON() { 156 return GeneratedSerializer._toJSON(); 157 } 158 } 159 else { 160 void populateFromJSON(std.json.JSONValue json) { 161 GeneratedDeserializer._fromJSON(json); 162 } 163 164 std.json.JSONValue convertToJSON() { 165 return GeneratedSerializer._toJSON(); 166 } 167 } 168 } 169 170 /// object serialization -- fields only 171 unittest { 172 import std.math : approxEqual; 173 174 static class Fields { 175 this() { } // class must have a no-args ctor 176 177 this(int iVal, float fVal, string sVal, int[] aVal, string noJson) { 178 i = iVal; 179 f = fVal; 180 s = sVal; 181 a = aVal; 182 dontJsonMe = noJson; 183 } 184 185 mixin JsonizeMe; 186 187 @jsonize { // fields to jsonize -- test different access levels 188 public int i; 189 protected float f; 190 public int[] a; 191 private string s; 192 } 193 string dontJsonMe; 194 195 override bool opEquals(Object o) { 196 auto other = cast(Fields) o; 197 return i == other.i && s == other.s && a == other.a && f.approxEqual(other.f); 198 } 199 } 200 201 auto obj = new Fields(1, 4.2, "tally ho!", [9, 8, 7, 6], "blarg"); 202 auto json = toJSON!Fields(obj); 203 204 assert(json.object["i"].integer == 1); 205 assert(json.object["f"].floating.approxEqual(4.2)); 206 assert(json.object["s"].str == "tally ho!"); 207 assert(json.object["a"].array[0].integer == 9); 208 assert("dontJsonMe" !in json.object); 209 210 // reconstruct from json 211 auto r = fromJSON!Fields(json); 212 assert(r.i == 1); 213 assert(r.f.approxEqual(4.2)); 214 assert(r.s == "tally ho!"); 215 assert(r.a == [9, 8, 7, 6]); 216 assert(r.dontJsonMe is null); 217 218 // array of objects 219 auto a = [ 220 new Fields(1, 4.2, "tally ho!", [9, 8, 7, 6], "blarg"), 221 new Fields(7, 42.2, "yea merrily", [1, 4, 6, 4], "asparagus") 222 ]; 223 224 // serialize array of user objects to json 225 auto jsonArray = toJSON!(Fields[])(a); 226 // reconstruct from json 227 assert(fromJSON!(Fields[])(jsonArray) == a); 228 } 229 230 /// object serialization with properties 231 unittest { 232 import std.math : approxEqual; 233 234 static class Props { 235 this() { } // class must have a no-args ctor 236 237 this(int iVal, float fVal, string sVal, string noJson) { 238 _i = iVal; 239 _f = fVal; 240 _s = sVal; 241 _dontJsonMe = noJson; 242 } 243 244 mixin JsonizeMe; 245 246 @property { 247 // jsonize ref property accessing private field 248 @jsonize ref int i() { return _i; } 249 // jsonize property with non-trivial get/set methods 250 @jsonize float f() { return _f - 3; } // the jsonized value will equal _f - 3 251 float f(float val) { return _f = val + 5; } // 5 will be added to _f when retrieving from json 252 // don't jsonize these properties 253 ref string s() { return _s; } 254 ref string dontJsonMe() { return _dontJsonMe; } 255 } 256 257 private: 258 int _i; 259 float _f; 260 @jsonize string _s; 261 string _dontJsonMe; 262 } 263 264 auto obj = new Props(1, 4.2, "tally ho!", "blarg"); 265 auto json = toJSON(obj); 266 267 assert(json.object["i"].integer == 1); 268 assert(json.object["f"].floating.approxEqual(4.2 - 3.0)); // property should have subtracted 3 on retrieval 269 assert(json.object["_s"].str == "tally ho!"); 270 assert("dontJsonMe" !in json.object); 271 272 auto r = fromJSON!Props(json); 273 assert(r.i == 1); 274 assert(r._f.approxEqual(4.2 - 3.0 + 5.0)); // property accessor should add 5 275 assert(r._s == "tally ho!"); 276 assert(r.dontJsonMe is null); 277 } 278 279 /// object serialization with custom constructor 280 unittest { 281 import std.conv : to; 282 import std.json : parseJSON; 283 import std.math : approxEqual; 284 import jsonizer.tojson : toJSON; 285 286 static class Custom { 287 mixin JsonizeMe; 288 289 this(int i) { 290 _i = i; 291 _s = "something"; 292 _f = 10.2; 293 } 294 295 @jsonize this(int _i, string _s, float _f = 20.2) { 296 this._i = _i; 297 this._s = _s ~ " jsonized"; 298 this._f = _f; 299 } 300 301 @jsonize this(double d) { // alternate ctor 302 _f = d.to!float; 303 _s = d.to!string; 304 _i = d.to!int; 305 } 306 307 private: 308 @jsonize { 309 string _s; 310 float _f; 311 int _i; 312 } 313 } 314 315 auto c = new Custom(12); 316 auto json = toJSON(c); 317 assert(json.object["_i"].integer == 12); 318 assert(json.object["_s"].str == "something"); 319 assert(json.object["_f"].floating.approxEqual(10.2)); 320 auto c2 = fromJSON!Custom(json); 321 assert(c2._i == 12); 322 assert(c2._s == "something jsonized"); 323 assert(c2._f.approxEqual(10.2)); 324 325 // test alternate ctor 326 json = parseJSON(`{"d" : 5}`); 327 c = json.fromJSON!Custom; 328 assert(c._f.approxEqual(5) && c._i == 5 && c._s == "5"); 329 } 330 331 /// struct serialization 332 unittest { 333 import std.math : approxEqual; 334 335 static struct S { 336 mixin JsonizeMe; 337 338 @jsonize { 339 int x; 340 float f; 341 string s; 342 } 343 int dontJsonMe; 344 345 this(int x, float f, string s, int noJson) { 346 this.x = x; 347 this.f = f; 348 this.s = s; 349 this.dontJsonMe = noJson; 350 } 351 } 352 353 auto s = S(5, 4.2, "bogus", 7); 354 auto json = toJSON(s); // serialize a struct 355 356 assert(json.object["x"].integer == 5); 357 assert(json.object["f"].floating.approxEqual(4.2)); 358 assert(json.object["s"].str == "bogus"); 359 assert("dontJsonMe" !in json.object); 360 361 auto r = fromJSON!S(json); 362 assert(r.x == 5); 363 assert(r.f.approxEqual(4.2)); 364 assert(r.s == "bogus"); 365 assert(r.dontJsonMe == int.init); 366 } 367 368 /// json file I/O 369 unittest { 370 import std.file : remove; 371 import jsonizer.fromjson : readJSON; 372 import jsonizer.tojson : writeJSON; 373 374 enum file = "test.json"; 375 scope(exit) remove(file); 376 377 static struct Data { 378 mixin JsonizeMe; 379 380 @jsonize { 381 int x; 382 string s; 383 float f; 384 } 385 } 386 387 // write an array of user-defined structs 388 auto array = [Data(5, "preposterous", 12.7), Data(8, "tesseract", -2.7), Data(5, "baby sloths", 102.7)]; 389 file.writeJSON(array); 390 auto readBack = file.readJSON!(Data[]); 391 assert(readBack == array); 392 393 // now try an associative array 394 auto aa = ["alpha": Data(27, "yams", 0), "gamma": Data(88, "spork", -99.999)]; 395 file.writeJSON(aa); 396 auto aaReadBack = file.readJSON!(Data[string]); 397 assert(aaReadBack == aa); 398 } 399 400 /// inheritance 401 unittest { 402 import std.math : approxEqual; 403 static class Parent { 404 mixin JsonizeMe; 405 @jsonize { 406 int x; 407 string s; 408 } 409 } 410 411 static class Child : Parent { 412 mixin JsonizeMe; 413 @jsonize { 414 float f; 415 } 416 } 417 418 auto c = new Child; 419 c.x = 5; 420 c.s = "hello"; 421 c.f = 2.1; 422 423 auto json = c.toJSON; 424 assert(json.fromJSON!int("x") == 5); 425 assert(json.fromJSON!string("s") == "hello"); 426 assert(json.fromJSON!float("f").approxEqual(2.1)); 427 428 auto child = json.fromJSON!Child; 429 assert(child.x == 5 && child.s == "hello" && child.f.approxEqual(2.1)); 430 431 auto parent = json.fromJSON!Parent; 432 assert(parent.x == 5 && parent.s == "hello"); 433 } 434 435 /// inheritance with ctors 436 unittest { 437 import std.math : approxEqual; 438 static class Parent { 439 mixin JsonizeMe; 440 441 @jsonize this(int x, string s) { 442 _x = x; 443 _s = s; 444 } 445 446 @jsonize @property { 447 int x() { return _x; } 448 string s() { return _s; } 449 } 450 451 private: 452 int _x; 453 string _s; 454 } 455 456 static class Child : Parent { 457 mixin JsonizeMe; 458 459 @jsonize this(int x, string s, float f) { 460 super(x, s); 461 _f = f; 462 } 463 464 @jsonize @property { 465 float f() { return _f; } 466 } 467 468 private: 469 float _f; 470 } 471 472 auto c = new Child(5, "hello", 2.1); 473 474 auto json = c.toJSON; 475 assert(json.fromJSON!int("x") == 5); 476 assert(json.fromJSON!string("s") == "hello"); 477 assert(json.fromJSON!float("f").approxEqual(2.1)); 478 479 auto child = json.fromJSON!Child; 480 assert(child.x == 5 && child.s == "hello" && child.f.approxEqual(2.1)); 481 482 auto parent = json.fromJSON!Parent; 483 assert(parent.x == 5 && parent.s == "hello"); 484 } 485 486 /// renamed members 487 unittest { 488 static class Bleh { 489 mixin JsonizeMe; 490 private { 491 @jsonize("x") int _x; 492 @jsonize("s") string _s; 493 } 494 } 495 496 auto b = new Bleh; 497 b._x = 5; 498 b._s = "blah"; 499 500 auto json = b.toJSON; 501 502 assert(json.fromJSON!int("x") == 5); 503 assert(json.fromJSON!string("s") == "blah"); 504 505 auto reconstruct = json.fromJSON!Bleh; 506 assert(reconstruct._x == b._x && reconstruct._s == b._s); 507 } 508 509 // members that potentially conflict with variables used in the mixin 510 unittest { 511 static struct Foo { 512 mixin JsonizeMe; 513 @jsonize int val; 514 } 515 516 Foo orig = Foo(3); 517 auto serialized = orig.toJSON; 518 auto deserialized = serialized.fromJSON!Foo; 519 520 assert(deserialized.val == orig.val); 521 } 522 523 /// unfortunately these test classes must be implemented outside the unittest 524 /// as Object.factory (and ClassInfo.find) cannot work with nested classes 525 private { 526 class TestComponent { 527 mixin JsonizeMe; 528 @jsonize int c; 529 } 530 531 class TestCompA : TestComponent { 532 mixin JsonizeMe; 533 @jsonize int a; 534 } 535 536 class TestCompB : TestComponent { 537 mixin JsonizeMe; 538 @jsonize string b; 539 } 540 } 541 542 /// type inference 543 unittest { 544 import std.json : parseJSON; 545 import std.string : format; 546 import std.traits : fullyQualifiedName; 547 548 // need to use these because unittest is assigned weird name 549 // normally would just be "modulename.classname" 550 string classKeyA = fullyQualifiedName!TestCompA; 551 string classKeyB = fullyQualifiedName!TestCompB; 552 553 assert(Object.factory(classKeyA) !is null && Object.factory(classKeyB) !is null, 554 "cannot factory classes in unittest -- this is a problem with the test"); 555 556 auto data = `[ 557 { 558 "class": "%s", 559 "c": 1, 560 "a": 5 561 }, 562 { 563 "class": "%s", 564 "c": 2, 565 "b": "hello" 566 } 567 ]`.format(classKeyA, classKeyB).parseJSON.fromJSON!(TestComponent[]); 568 569 auto a = cast(TestCompA) data[0]; 570 auto b = cast(TestCompB) data[1]; 571 572 assert(a !is null && a.c == 1 && a.a == 5); 573 assert(b !is null && b.c == 2 && b.b == "hello"); 574 } 575 576 // TODO: These are not examples but edge-case tests 577 // factor out into dedicated test modules 578 579 // Validate issue #20: 580 // Unable to de-jsonize a class when a construct is marked @jsonize. 581 unittest { 582 import std.json : parseJSON; 583 import std.algorithm : canFind; 584 import std.exception : collectException; 585 import jsonizer.jsonize : jsonize, JsonizeMe; 586 import jsonizer.exceptions : JsonizeConstructorException; 587 import jsonizer.fromjson : fromJSON; 588 589 static class A { 590 private const int a; 591 592 this(float f) { 593 a = 0; 594 } 595 596 @jsonize this(int a) { 597 this.a = a; 598 } 599 600 @jsonize this(string s, float f) { 601 a = 0; 602 } 603 } 604 605 auto ex = collectException!JsonizeConstructorException(`{}`.parseJSON.fromJSON!A); 606 assert(ex !is null, "failure to match @jsonize'd constructors should throw"); 607 assert(ex.msg.canFind("(int a)") && ex.msg.canFind("(string s, float f)"), 608 "JsonizeConstructorException message should contain attempted constructors"); 609 assert(!ex.msg.canFind("(float f)"), 610 "JsonizeConstructorException message should not contain non-jsonized constructors"); 611 } 612 613 // Validate issue #17: 614 // Unable to construct class containing private (not marked with @jsonize) types. 615 unittest { 616 import std.json : parseJSON; 617 618 static class A { 619 mixin JsonizeMe; 620 621 private int a; 622 623 @jsonize public this(int a) { 624 this.a = a; 625 } 626 } 627 628 auto json = `{ "a": 5}`.parseJSON; 629 auto a = fromJSON!A(json); 630 631 assert(a.a == 5); 632 } 633 634 // Validate issue #18: 635 // Unable to construct class with const types. 636 unittest { 637 import std.json : parseJSON; 638 639 static class A { 640 mixin JsonizeMe; 641 642 const int a; 643 644 @jsonize public this(int a) { 645 this.a = a; 646 } 647 } 648 649 auto json = `{ "a": 5}`.parseJSON; 650 auto a = fromJSON!A(json); 651 652 assert(a.a == 5); 653 } 654 655 // Validate issue #19: 656 // Unable to construct class containing private (not marked with @jsonize) types. 657 unittest { 658 import std.json : parseJSON; 659 660 static class A { 661 mixin JsonizeMe; 662 663 alias Integer = int; 664 Integer a; 665 666 @jsonize public this(Integer a) { 667 this.a = a; 668 } 669 } 670 671 auto json = `{ "a": 5}`.parseJSON; 672 auto a = fromJSON!A(json); 673 674 assert(a.a == 5); 675 }