1 module jsonwrap; 2 /++ Some extra functions to make easier to work with json 3 --- 4 import jsonwrap; 5 6 void main() 7 { 8 import std.exception :assertThrown; 9 10 // This works with CTFE, too. 11 auto j = JSOB( 12 "field1", "value1", 13 "field2", JSOB( 14 "subfield1", "value2", 15 "subfield2", 3, 16 "subfield3", [1,2,3], 17 ), 18 "field3", JSAB("mixed", 10, "array", JSOB("obj", 15)) 19 ); 20 21 // Or 22 // auto j = parseJSON(`{"field1" : "value1", "field2" : "..."}`); 23 24 // Read will throw on error 25 assert(j.read!string("/field2/subfield1") == "value2"); 26 assert(j.read!int("/field3/1") == 10); 27 assert(j.read!int("/field3/3/obj") == 15); 28 assertThrown(j.read!string("/field2/subfield2")); // Wrong type 29 30 // Safe return default value on error 31 assert(j.safe!string("/field2/subfield2") == string.init); // subfield2 is a int, wrong type. 32 assert(j.safe!string("/field2/wrong/path") == string.init); 33 assert(j.safe!string("/field2/wrong/path", "default") == "default"); 34 35 // Like safe, but it tries to convert 36 assert(j.as!string("/field2/subfield1"), "value2"); 37 assert(j.as!string("/field2/subfield2"), "3"); 38 39 // Check if a key exists 40 assert(j.exists("/field2/subfield1") == true); 41 assert(j.exists("/field3/test") == false); 42 43 // Remove a key 44 assert(j.exists("/field2/subfield2") == true); 45 j.remove("/field2/subfield2"); 46 assert(j.exists("/field2/subfield2") == false); 47 48 // Add a new value, recreating the whole tree 49 j.put("hello/world/so/deep", "yay!"); 50 assert(j.exists("hello/world/so/deep") == true); 51 52 } 53 --- 54 +/ 55 56 import std.json : JSONValue, JSONType; 57 import std.string : isNumeric, indexOf; 58 import std.typecons : Tuple; 59 import std.conv : to; 60 import std.traits : isIntegral, isSomeString; 61 62 alias JSOB = JsonObjectBuilder; /// Shorthand 63 alias JSAB = JsonArrayBuilder; /// Shorthand 64 65 deprecated(".get() is deprecated. Use .safe() instead") 66 { 67 alias get = jsonwrap.safe; 68 } 69 70 /// A simple struct. It is returned by .safe and .as functions 71 struct SafeValue(T) 72 { 73 @safe @nogc nothrow 74 this(in bool exists, in bool ok, in T value = T.init) 75 { 76 this._exists = exists; 77 this._ok = ok; 78 this.value = value; 79 } 80 81 // Return true if key is found. 82 @property @safe @nogc nothrow exists() inout { return _exists; } 83 84 // Return true if value is read without errors 85 @property @safe @nogc nothrow ok() inout { return _ok; } 86 87 private bool _exists = false; 88 private bool _ok = false; 89 90 T value = T.init; 91 92 alias value this; 93 } 94 95 /// It allows you to read deep values inside a json. If possibile it converts value to type T. 96 pure nothrow 97 SafeValue!T as(T)(in JSONValue json, in string path = "", /* lazy */ in T defaultValue = T.init) 98 { 99 // A way to check if to!T is valid 100 pure 101 void tryConv(T, K)(in K value, ref SafeValue!T result) 102 { 103 static if (__traits(compiles,to!T(value))) 104 { 105 result.value = to!T(value); 106 result._ok = true; 107 } 108 else 109 { 110 result.value = defaultValue; 111 result._ok = false; 112 113 } 114 } 115 116 immutable splitted = split_json_path(path); 117 immutable isLast = splitted.remainder.length == 0; 118 JSONValue value; 119 120 // Split the path passed in tokens and take the first JSONValue 121 try 122 { 123 if (json.type() == JSONType.object) value = json[splitted.token]; 124 else if (json.type() == JSONType.array) value = json[to!size_t(splitted.token)]; 125 else value = json; 126 } 127 catch (Exception e) { return SafeValue!T(false, false, defaultValue); } 128 129 immutable type = value.type(); 130 131 // The token is a leaf on json, but it's not a leaf on requested path 132 if (!isLast && type != JSONType.array && type != JSONType.object) 133 return SafeValue!T(false, false, defaultValue); 134 135 SafeValue!T result = SafeValue!T(true, true, defaultValue); 136 137 try 138 { 139 final switch(type) 140 { 141 case JSONType.null_: 142 result._ok = is(T == typeof(null)); 143 break; 144 145 case JSONType.false_: 146 static if (is(T == bool)) result.value = false; 147 else tryConv!T(false, result); 148 break; 149 150 case JSONType.true_: 151 static if (is(T == bool)) result.value = true; 152 else tryConv!T(true, result); 153 break; 154 155 case JSONType.float_: 156 static if (is(T == float) || is(T == double)) result.value = to!T(value.floating()); 157 else tryConv!T(value.floating(), result); 158 break; 159 160 case JSONType.integer: 161 static if (isIntegral!T) result.value = to!T(value.integer()); 162 else tryConv!T(value.integer(), result); 163 break; 164 165 case JSONType.uinteger: 166 static if (isIntegral!T) result.value = to!T(value.uinteger()); 167 else tryConv!T(value.uinteger(), result); 168 break; 169 170 case JSONType..string: 171 static if (isSomeString!T) result.value = to!T(value.str()); 172 else tryConv!T(value.str(), result); 173 break; 174 175 case JSONType.object: 176 if (isLast) 177 { 178 // We are on the last token of path and we have a object. If user asks for a JSONValue it's ok. 179 static if (is(T == JSONValue)) result.value = value.object(); 180 else result._ok = false; 181 } 182 // Recursion: read next part of path 183 else return as!T(value, splitted.remainder, defaultValue); 184 break; 185 186 // Ricorsivo: richiamo per l'elemento indicizzato con il percorso accorciato 187 case JSONType.array: 188 if (isLast) 189 { 190 // We are on the last token of path and we have an array. If user asks for a JSONValue it's ok. 191 static if (is(T == JSONValue)) result.value = value.array(); 192 else result._ok = false; 193 } 194 // Recursion: read next part of path 195 else return as!T(value, splitted.remainder, defaultValue); 196 break; 197 } 198 199 } 200 catch (Exception ce) 201 { 202 // Something goes wrong with conversions. Sorry, we give you back a default value 203 return SafeValue!T(true, false, defaultValue); 204 } 205 206 return result; 207 } 208 209 /// Shortcut. You can write as!null instead of as!(typeof(null)) 210 pure nothrow 211 SafeValue!(typeof(null)) as(typeof(null) T)(in JSONValue json, in string path = "") 212 { 213 return as!(typeof(null))(json, path); 214 } 215 216 unittest 217 { 218 immutable js = JSOB("string", "str", "null", null, "obj", JSOB("int", 1, "float", 3.0f, "arr", JSAB("1", 2))); 219 220 assert(js.as!(typeof(null))("null").ok == true); 221 assert(js.as!(typeof(null))("string").ok == false); 222 assert(js.as!string("/string") == "str"); 223 assert(js.as!string("/obj/int") == "1"); 224 assert(js.as!int("/obj/arr/0") == 1); 225 assert(js.as!int("/obj/arr/1") == 2); 226 assert(js.as!float("/obj/float") == 3.0f); 227 assert(js.as!int("/obj/int/blah").exists == false); 228 assert(js.as!string("bau").exists == false); 229 assert(js.as!int("/string").exists == true); 230 assert(js.as!int("/string").ok == false); 231 } 232 233 /// Works like as!T but it doesn't convert between types. 234 pure nothrow 235 SafeValue!T safe(T)(in JSONValue json, in string path = "", in T defaultValue = T.init) 236 { 237 alias Ret = SafeValue!T; 238 239 immutable splitted = split_json_path(path); 240 immutable isLast = splitted.remainder.length == 0; 241 JSONValue value; 242 243 // Split the path passed in tokens and take the first JSONValue 244 try 245 { 246 if (json.type() == JSONType.object) value = json[splitted.token]; 247 else if (json.type() == JSONType.array) value = json[to!size_t(splitted.token)]; 248 else value = json; 249 } 250 catch (Exception e) 251 { 252 return Ret(false, false, defaultValue); 253 } 254 255 immutable type = value.type(); 256 257 // The token is a leaf on json, but it's not a leaf on requested path 258 if (!isLast && type != JSONType.array && type != JSONType.object) 259 return Ret(false, false, defaultValue); 260 261 try 262 { 263 final switch(type) 264 { 265 case JSONType.null_: static if (is(T == typeof(null))) return Ret(true, true, null); else break; 266 case JSONType.false_: static if (is(T == bool)) return Ret(true, true, false); else break; 267 case JSONType.true_: static if (is(T == bool)) return Ret(true, true, true); else break; 268 case JSONType.float_: static if (is(T == float) || is(T == double)) return Ret(true, true, value.floating()); else break; 269 case JSONType.integer: static if (isIntegral!T) return Ret(true, true, to!T(value.integer())); else break; 270 case JSONType.uinteger: static if (isIntegral!T) return Ret(true, true, to!T(value.uinteger())); else break; 271 case JSONType..string: static if (isSomeString!T) return Ret(true, true, value.str()); else break; 272 273 case JSONType.object: 274 if (isLast) { 275 // See also: as!T 276 static if (is(T == JSONValue)) 277 return Ret(true, true, JSONValue(value.object)); 278 else break; 279 } 280 else return safe!T(value, splitted.remainder, defaultValue); 281 282 case JSONType.array: 283 if (isLast) { 284 // See also: as!T 285 static if (is(T == JSONValue)) 286 return Ret(true, true, JSONValue(value.array)); 287 else break; 288 } 289 else return safe!T(value, splitted.remainder, defaultValue); 290 } 291 } 292 catch (Exception e) 293 { 294 return Ret(true, false, defaultValue); 295 } 296 297 // Wrong conversion requested. 298 return Ret(true, false, defaultValue); 299 } 300 301 /// Shortcut. You can write safe!null instead of safe!(typeof(null)) 302 pure nothrow 303 SafeValue!(typeof(null)) safe(typeof(null) T)(in JSONValue json, in string path = "") 304 { 305 return safe!(typeof(null))(json, path); 306 } 307 308 unittest 309 { 310 immutable js = JSOB("string", "str", "null", null, "obj", JSOB("int", 1, "float", 3.0f, "arr", JSAB("1", 2))); 311 312 assert(js.safe!(typeof(null))("null").ok == true); 313 assert(js.safe!(typeof(null))("string").ok == false); 314 assert(js.safe!string("/string") == "str"); 315 316 assert(js.safe!string("/obj/int").ok == false); 317 assert(js.safe!string("/obj/int") == string.init); 318 319 assert(js.safe!int("/obj/arr/0").ok == false); 320 assert(js.safe!int("/obj/arr/0") == int.init); 321 322 assert(js.safe!int("/obj/arr/1") == 2); 323 assert(js.safe!float("/obj/float") == 3.0f); 324 assert(js.safe!int("/obj/int/blah").exists == false); 325 assert(js.safe!string("bau").exists == false); 326 assert(js.safe!int("/string").exists == true); 327 assert(js.safe!int("/string").ok == false); 328 } 329 330 unittest 331 { 332 immutable js = JSOB("notnull", 0, "null", null); 333 334 assert(js.as!null("/null").ok == true); 335 assert(js.as!null("/notnull").ok == false); 336 337 assert(js.safe!null("/null").ok == true); 338 assert(js.safe!null("/notnull").ok == false); 339 } 340 341 /// Works like safe but return T instead of SafeValue!T and throw an exception if something goes wrong (can't convert value or can't find key) 342 pure 343 T read(T)(in JSONValue json, in string path = "") 344 { 345 auto ret = safe!T(json, path); 346 347 if (!ret.ok || !ret.exists) 348 throw new Exception("Can't read " ~ path ~ " from json"); 349 350 return ret.value; 351 } 352 353 unittest 354 { 355 import std.exception: assertThrown; 356 immutable js = JSOB("string", "str", "null", null, "obj", JSOB("int", 1, "float", 3.0f, "arr", JSAB("1", 2))); 357 358 assert(js.read!string("string") == "str"); 359 assert(js.read!int("/obj/int") == 1); 360 assertThrown(js.read!int("string")); 361 assertThrown(js.read!int("other")); 362 } 363 364 365 /// Write a value. It creates missing objects and array (also missing elements) 366 pure 367 ref JSONValue put(T)(return ref JSONValue json, in string path, in T value) 368 { 369 // Take a token from path 370 immutable splitted = split_json_path(path); 371 immutable isLast = splitted.remainder.length == 0; 372 373 enum nullValue = JSONValue(null); 374 375 // If token is a number, we are trying to write an array. 376 if (isNumeric(splitted.token)) 377 { 378 immutable idx = to!size_t(splitted.token); 379 380 // Are we reading an existing element from an existing array? 381 if (json.type == JSONType.array && json.array.length > idx) 382 { 383 if (!isLast) put!T(json.array[idx], splitted.remainder, value); 384 else json.array[idx] = value; 385 } 386 else 387 { 388 if (json.type != JSONType.array) 389 json = JSONValue[].init; 390 391 json.array.length = idx+1; 392 393 if (!isLast) put!T(json.array[idx], splitted.remainder, value); 394 else json.array[idx] = value; 395 } 396 } 397 // If token is *NOT* a number, we are trying to write an object. 398 else 399 { 400 immutable idx = splitted.token; 401 402 // Are we reading an existing object? 403 if (json.type == JSONType.object) 404 { 405 if (!isLast) 406 { 407 if (idx !in json.object) 408 json.object[idx] = nullValue; 409 410 put!T(json.object[idx], splitted.remainder, value); 411 } 412 else json.object[idx] = value; 413 } 414 else 415 { 416 json = string[string].init; 417 418 if (!isLast) 419 { 420 json.object[idx] = nullValue; 421 put!T(json.object[idx], splitted.remainder, value); 422 } 423 else json.object[idx] = value; 424 } 425 } 426 427 return json; 428 } 429 430 unittest 431 { 432 auto js = JSOB("string", "str", "null", null, "obj", JSOB("int", 1, "float", 3.0f, "arr", JSAB("1", 2))); 433 434 js.put("/string", "hello"); 435 js.put("/null/not", 10); 436 js.put("/obj/arr/3", JSOB); 437 js.put("hello", "world"); 438 439 assert(js.safe!string("/string") == "hello"); 440 assert(js.safe!int("/null/not") == 10); 441 assert(js.safe!null("/obj/arr/2").ok); 442 assert(js.safe!JSONValue("/obj/arr/3") == JSOB); 443 assert(js.safe!JSONValue("/obj/arr/3").ok == true); 444 assert(js.safe!string("hello") == "world"); 445 } 446 447 /// Remove a field (if it exists). It returns the object itself 448 pure 449 ref JSONValue remove(return ref JSONValue json, in string path) 450 { 451 immutable splitted = split_json_path(path); 452 immutable isLast = splitted.remainder.length == 0; 453 454 // See above 455 if (isNumeric(splitted.token)) 456 { 457 immutable idx = to!size_t(splitted.token); 458 459 if (json.type == JSONType.array && json.array.length > idx) 460 { 461 if (isLast) json.array = json.array[0..idx] ~ json.array[idx+1 .. $]; 462 else json.array[idx].remove(splitted.remainder); 463 } 464 465 } 466 else 467 { 468 immutable idx = splitted.token; 469 470 if (json.type == JSONType.object && idx in json.object) 471 { 472 if (isLast) json.object.remove(idx); 473 else json.object[idx].remove(splitted.remainder); 474 } 475 } 476 477 478 return json; 479 } 480 481 /// Check if a field exists or not 482 pure 483 bool exists(in JSONValue json, in string path) 484 { 485 immutable splitted = split_json_path(path); 486 immutable isLast = splitted.remainder.length == 0; 487 488 // See above 489 if (isNumeric(splitted.token)) 490 { 491 immutable idx = to!size_t(splitted.token); 492 493 if (json.type == JSONType.array && json.array.length > idx) 494 { 495 if (isLast) return true; 496 else return json.array[idx].exists(splitted.remainder); 497 } 498 499 } 500 else 501 { 502 immutable idx = splitted.token; 503 504 if (json.type == JSONType.object && idx in json.object) 505 { 506 if (isLast) return true; 507 else return json.object[idx].exists(splitted.remainder); 508 } 509 } 510 511 return false; 512 } 513 514 unittest 515 { 516 auto js = JSOB("string", "str", "null", null, "obj", JSOB("int", 1, "float", 3.0f, "arr", JSAB("1", 2))); 517 518 js.put("/string", "hello"); 519 js.put("/null/not", 10); 520 js.put("/obj/arr/3", JSOB); 521 js.put("hello", "world"); 522 523 js.remove("/obj/arr/2"); 524 js.remove("string"); 525 526 assert(js.exists("/string") == false); 527 assert(js.exists("/obj/arr/3") == false); 528 assert(js.exists("/obj/arr/2") == true); 529 assert(js.safe!JSONValue("/obj/arr/2") == JSOB); 530 } 531 532 533 private alias SplitterResult = Tuple!(string, "token", string, "remainder"); 534 535 /// Used to split path like /hello/world in tokens 536 pure nothrow @safe @nogc 537 private SplitterResult split_json_path(in string path) 538 { 539 immutable idx = path.indexOf('/'); 540 541 switch (idx) 542 { 543 case 0: return split_json_path(path[1..$]); 544 case -1: return SplitterResult(path, string.init); 545 default: return SplitterResult(path[0..idx], path[idx+1..$]); 546 } 547 548 assert(0); 549 } 550 551 /// You can build a json object with JsonObjectBuilder("key", 32, "another_key", "hello", "subobject", JsonObjectBuilder(...)); 552 pure 553 JSONValue JsonObjectBuilder(T...)(T vals) 554 { 555 void appendJsonVals(T...)(ref JSONValue value, T vals) 556 { 557 // Appends nothing, recursion ends 558 static if (vals.length == 0) return; 559 560 // We're working with a tuple (key, value, key, value, ...) so args%2==0 is key and args%2==1 is value 561 else static if (vals.length % 2 == 0) 562 { 563 // Key should be a string! 564 static if (!isSomeString!(typeof(vals[0]))) 565 throw new Exception("Wrong param type. Key not valid."); 566 567 else value[vals[0]] = vals[1]; 568 569 // Recursion call 570 static if (vals.length > 2) 571 appendJsonVals(value, vals[2..$]); 572 573 } else throw new Exception("Wrong params. Should be: JsonObjectBuilder(string key1, T1 val1, string key2, T2 val2, ...)"); 574 } 575 576 JSONValue value = string[string].init; 577 578 static if (vals.length > 0) 579 appendJsonVals(value, vals); 580 581 return value; 582 } 583 584 /// You can build a json array with JsonArrayBuilder("first", 32, "another_element", 2, 23.4, JsonObjectBuilder(...)); 585 pure 586 JSONValue JsonArrayBuilder(T...)(T vals) 587 { 588 JSONValue value = JSONValue[].init; 589 value.array.length = vals.length; 590 591 foreach(idx, v; vals) 592 value[idx] = v; 593 594 return value; 595 } 596 597 unittest 598 { 599 { 600 enum js = JSOB("array", JSAB(1,2,"blah"), "subobj", JSOB("int", 1, "string", "str", "array", [1,2,3])); 601 assert(js.safe!int("/array/1") == 2); 602 assert(js.safe!int("/subobj/int") == 1); 603 assert(js.safe!string("/subobj/string") == "str"); 604 assert(js.as!string("/subobj/array/2") == "3"); 605 assert(js.exists("/subobj/string") == true); 606 assert(js.exists("/subobj/other") == false); 607 608 // /array/1 it's an integer 609 { 610 // Can't get a string 611 { 612 immutable val = js.safe!string("/array/1", "default"); 613 assert(val.exists == true); 614 assert(val.ok == false); 615 assert(val == "default"); 616 } 617 618 // Can read as string 619 { 620 immutable val = js.as!string("/array/1", "default"); 621 assert(val.exists == true); 622 assert(val.ok == true); 623 assert(val == "2"); 624 } 625 } 626 627 628 // This value doesn't exist 629 { 630 immutable val = js.as!string("/subobj/other", "default"); 631 assert(val.exists == false); 632 assert(val.ok == false); 633 assert(val == "default"); 634 } 635 636 637 // Value exists but can't convert to int 638 { 639 immutable val = js.as!int("/array/2", 15); 640 assert(val.exists == true); 641 assert(val.ok == false); 642 assert(val == 15); 643 } 644 645 // Can't edit an enum, of course 646 assert(__traits(compiles, js.remove("/subobj/string")) == false); 647 648 // But I can edit a copy 649 JSONValue cp = js; 650 assert(cp == js); 651 assert(cp.toString == js.toString); 652 653 cp.remove("/subobj/string"); 654 assert(cp.exists("/subobj/string") == false); 655 assert(cp.exists("/subobj/int") == true); 656 657 } 658 } 659 660 661 unittest 662 { 663 import std.json : parseJSON; 664 665 // Standard way 666 JSONValue json = parseJSON(`{"user" : "foo", "address" : {"city" : "venice", "country" : "italy"}, "tags" : ["hello" , 3 , {"key" : "value"}]}`); 667 668 { 669 string user = json.safe!string("user"); // Read a string from json 670 assert(user == "foo"); 671 } 672 673 { 674 // Read a string, user is a SafeValue!string 675 auto user = json.safe!string("user"); 676 assert(user.ok == true); 677 assert(user.exists == true); 678 679 // This field doesn't exists on json 680 // I can set a default value 681 auto notfound = json.safe!string("blah", "my default value"); 682 assert(notfound.ok == false); 683 assert(notfound.exists == false); 684 assert(notfound == "my default value"); 685 686 // This field exists but it's not an int, it's a string 687 auto wrong = json.safe!int("user"); 688 assert(wrong.ok == false); 689 assert(wrong.exists == true); 690 assert(wrong == int.init); 691 } 692 693 { 694 // I can read deep fields 695 assert(json.safe!string("/address/city") == "venice"); 696 697 // also inside an array 698 assert(json.safe!string("/tags/2/key") == "value"); 699 } 700 701 { 702 // Using as!T you can convert field 703 assert(json.as!string("/tags/1") == "3"); // On json "/tags/1" is an int. 704 } 705 706 { 707 // You can check if a field exists or not 708 assert(json.exists("/address/country") == true); 709 710 // You can remove it 711 json.remove("/address/country"); 712 713 // It doesn't exists anymore 714 assert(json.exists("/address/country") == false); 715 } 716 717 { 718 // You can write using put. 719 json.put("/address/country", "italy"); // Restore deleted field 720 json.put("/this/is/a/deep/value", 100); // It create the whole tree 721 json.put("/this/is/an/array/5", "hello"); // Ditto 722 723 assert(json.safe!int("/this/is/a/deep/value") == 100); 724 assert(json.safe!string("/this/is/an/array/5") == "hello"); // elements 0,1,2,3,4 are nulled 725 } 726 727 { 728 // A fast way to build object CTFE compatible. 729 // JSOB is an alias for JsonObjectBuilder and JSAB for JsonArrayBuilder 730 JSONValue jv = JSOB 731 ( 732 "key", "value", 733 "obj", JSOB("subkey", 3), 734 "array", [1,2,3], 735 "mixed_array", JSAB(1, "hello", 3.0f) 736 ); 737 738 assert(jv.toString == `{"array":[1,2,3],"key":"value","mixed_array":[1,"hello",3.0],"obj":{"subkey":3}}`); 739 } 740 741 { 742 JSONValue jv = JSOB 743 ( 744 "key", "value", 745 "obj", JSOB("subkey", 3), 746 "array", [1,2,3], 747 "mixed_array", JSAB(1, "hello", 3.0f) 748 ); 749 750 foreach(size_t idx, o; jv.safe!JSONValue("/array")) 751 { 752 assert(o.safe!int("/") == idx+1); 753 assert(o.as!float("") == idx+1); 754 assert(o.read!int("/")== idx+1); 755 assert(o.safe!int == idx+1); 756 assert(o.as!float == idx+1); 757 assert(o.read!int == idx+1); 758 } 759 } 760 } 761 762 /// Append value to array. Array is created if not exists. 763 pure 764 ref JSONValue append(T)(return ref JSONValue json, in string path, in T value) 765 { 766 auto r = json.as!JSONValue(path); 767 768 if (!r.exists) json.put(path ~ "/0", value); 769 else if (r.value.type != JSONType.array) json.put(path, JSAB(r.value, value)); 770 else json.put(path ~ "/" ~ r.array.length.to!string, value); 771 772 return json; 773 } 774 775 unittest 776 { 777 auto j = JSOB("k-1", "v-1", "k-2", 1); 778 779 j.put("/k-3/0", "v-3-1"); 780 j.put("/k-3/1", "v-3-2"); 781 782 j.append("/k-3", "v-3-3"); 783 j.append("k-4", "v-4-1"); 784 j.append("k-4", "v-4-2"); 785 j.append("k-2", "v-2-2"); 786 787 assert(j.toString == `{"k-1":"v-1","k-2":[1,"v-2-2"],"k-3":["v-3-1","v-3-2","v-3-3"],"k-4":["v-4-1","v-4-2"]}`); 788 } 789 790 /// Parse a string 791 pure 792 ref JSONValue parse(return ref JSONValue json, in string data) 793 { 794 import std.json : parseJSON; 795 json = parseJSON(data); 796 return json; 797 }