1 /** 2 * Contains functions for deserializing JSON data. 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.fromjson; 10 11 import std.json; 12 import std.conv; 13 import std.file; 14 import std.range; 15 import std.traits; 16 import std.string; 17 import std.algorithm; 18 import std.exception; 19 import std.typetuple; 20 import std.typecons : staticIota, Tuple; 21 import jsonizer.exceptions : JsonizeTypeException, JsonizeConstructorException; 22 import jsonizer.internal.attribute; 23 import jsonizer.internal.util; 24 25 /// json member used to map a json object to a D type 26 enum jsonizeClassKeyword = "class"; 27 28 private void enforceJsonType(T)(JSONValue json, JSON_TYPE[] expected ...) { 29 if (!expected.canFind(json.type)) { 30 throw new JsonizeTypeException(typeid(T), json, expected); 31 } 32 } 33 34 unittest { 35 import std.exception : assertThrown, assertNotThrown; 36 with (JSON_TYPE) { 37 assertThrown!JsonizeTypeException(enforceJsonType!int(JSONValue("hi"), INTEGER, UINTEGER)); 38 assertThrown!JsonizeTypeException(enforceJsonType!(bool[string])(JSONValue([ 5 ]), OBJECT)); 39 assertNotThrown(enforceJsonType!int(JSONValue(5), INTEGER, UINTEGER)); 40 assertNotThrown(enforceJsonType!(bool[string])(JSONValue(["key": true]), OBJECT)); 41 } 42 } 43 44 deprecated("use fromJSON instead") { 45 /// Deprecated: use `fromJSON` instead. 46 T extract(T)(JSONValue json) { 47 return json.fromJSON!T; 48 } 49 } 50 51 /// Extract a boolean from a json value. 52 T fromJSON(T : bool)(JSONValue json) { 53 if (json.type == JSON_TYPE.TRUE) { 54 return true; 55 } 56 else if (json.type == JSON_TYPE.FALSE) { 57 return false; 58 } 59 60 // expected 'true' or 'false' 61 throw new JsonizeTypeException(typeid(bool), json, JSON_TYPE.TRUE, JSON_TYPE.FALSE); 62 } 63 64 /// Extract booleans from json values. 65 unittest { 66 assert(JSONValue(false).fromJSON!bool == false); 67 assert(JSONValue(true).fromJSON!bool == true); 68 } 69 70 /// Extract a string type from a json value. 71 T fromJSON(T : string)(JSONValue json) { 72 if (json.type == JSON_TYPE.NULL) { return null; } 73 enforceJsonType!T(json, JSON_TYPE.STRING); 74 return cast(T) json.str; 75 } 76 77 /// Extract a string from a json string. 78 unittest { 79 assert(JSONValue("asdf").fromJSON!string == "asdf"); 80 } 81 82 /// Extract a numeric type from a json value. 83 T fromJSON(T : real)(JSONValue json) if (!is(T == enum)) { 84 switch(json.type) { 85 case JSON_TYPE.FLOAT: 86 return cast(T) json.floating; 87 case JSON_TYPE.INTEGER: 88 return cast(T) json.integer; 89 case JSON_TYPE.UINTEGER: 90 return cast(T) json.uinteger; 91 case JSON_TYPE.STRING: 92 enforce(json.str.isNumeric, format("tried to extract %s from json string %s", T.stringof, json.str)); 93 return to!T(json.str); // try to parse string as int 94 default: 95 } 96 97 throw new JsonizeTypeException(typeid(bool), json, 98 JSON_TYPE.FLOAT, JSON_TYPE.INTEGER, JSON_TYPE.UINTEGER, JSON_TYPE.STRING); 99 } 100 101 /// Extract various numeric types. 102 unittest { 103 assert(JSONValue(1).fromJSON!int == 1); 104 assert(JSONValue(2u).fromJSON!uint == 2u); 105 assert(JSONValue(3.0).fromJSON!double == 3.0); 106 107 // fromJSON accepts numeric strings when a numeric conversion is requested 108 assert(JSONValue("4").fromJSON!long == 4L); 109 } 110 111 /// Extract an enumerated type from a json value. 112 T fromJSON(T)(JSONValue json) if (is(T == enum)) { 113 enforceJsonType!T(json, JSON_TYPE.STRING); 114 return to!T(json.str); 115 } 116 117 /// Convert a json string into an enum value. 118 unittest { 119 enum Category { one, two } 120 assert(JSONValue("one").fromJSON!Category == Category.one); 121 } 122 123 /// Extract an array from a JSONValue. 124 T fromJSON(T)(JSONValue json) if (isArray!T && !isSomeString!(T)) { 125 if (json.type == JSON_TYPE.NULL) { return T.init; } 126 enforceJsonType!T(json, JSON_TYPE.ARRAY); 127 alias ElementType = ForeachType!T; 128 T vals; 129 foreach(idx, val ; json.array) { 130 static if (isStaticArray!T) { 131 vals[idx] = val.fromJSON!ElementType; 132 } 133 else { 134 vals ~= val.fromJSON!ElementType; 135 } 136 } 137 return vals; 138 } 139 140 /// Convert a json array into an array. 141 unittest { 142 auto a = [ 1, 2, 3 ]; 143 assert(JSONValue(a).fromJSON!(int[]) == a); 144 } 145 146 /// Extract an associative array from a JSONValue. 147 T fromJSON(T)(JSONValue json) if (isAssociativeArray!T) { 148 static assert(is(KeyType!T : string), "toJSON requires string keys for associative array"); 149 if (json.type == JSON_TYPE.NULL) { return null; } 150 enforceJsonType!T(json, JSON_TYPE.OBJECT); 151 alias ValType = ValueType!T; 152 T map; 153 foreach(key, val ; json.object) { 154 map[key] = fromJSON!ValType(val); 155 } 156 return map; 157 } 158 159 /// Convert a json object to an associative array. 160 unittest { 161 auto aa = ["a": 1, "b": 2]; 162 assert(JSONValue(aa).fromJSON!(int[string]) == aa); 163 } 164 165 /// Extract a value from a json object by its key. 166 T fromJSON(T)(JSONValue json, string key) { 167 enforceJsonType!T(json, JSON_TYPE.OBJECT); 168 enforce(key in json.object, "tried to extract non-existent key " ~ key ~ " from JSONValue"); 169 return fromJSON!T(json.object[key]); 170 } 171 172 unittest { 173 auto aa = ["a": 1, "b": 2]; 174 auto json = JSONValue(aa); 175 assert(json.fromJSON!int("a") == 1); 176 assert(json.fromJSON!ulong("b") == 2L); 177 } 178 179 /// Extract a value from a json object by its key, return `defaultVal` if key not found. 180 T fromJSON(T)(JSONValue json, string key, T defaultVal) { 181 enforceJsonType!T(json, JSON_TYPE.OBJECT); 182 return (key in json.object) ? fromJSON!T(json.object[key]) : defaultVal; 183 } 184 185 /// Substitute default values when keys aren't present. 186 unittest { 187 auto aa = ["a": 1, "b": 2]; 188 auto json = JSONValue(aa); 189 assert(json.fromJSON!int("a", 7) == 1); 190 assert(json.fromJSON!int("c", 7) == 7); 191 } 192 193 /// Convert a json value in its string representation into a type `T` 194 /// Params: 195 /// T = target type 196 /// json = json string to deserialize 197 T fromJSONString(T)(string json) { 198 return fromJSON!T(json.parseJSON); 199 } 200 201 unittest { 202 assert(fromJSONString!(int[])("[1, 2, 3]") == [1, 2, 3]); 203 } 204 205 /// Read a json-constructable object from a file. 206 /// Params: 207 /// path = filesystem path to json file 208 /// Returns: object parsed from json file 209 T readJSON(T)(string path) { 210 auto json = parseJSON(readText(path)); 211 return fromJSON!T(json); 212 } 213 214 /// Read a json file directly into a specified D type. 215 unittest { 216 import std.path : buildPath; 217 import std.uuid : randomUUID; 218 import std.file : tempDir, write, mkdirRecurse; 219 220 auto dir = buildPath(tempDir(), "jsonizer_readjson_test"); 221 mkdirRecurse(dir); 222 auto file = buildPath(dir, randomUUID().toString); 223 224 file.write("[1, 2, 3]"); 225 226 assert(file.readJSON!(int[]) == [ 1, 2, 3 ]); 227 } 228 229 /// Read contents of a json file directly into a JSONValue. 230 /// Params: 231 /// path = filesystem path to json file 232 /// Returns: a `JSONValue` parsed from the file 233 auto readJSON(string path) { 234 return parseJSON(readText(path)); 235 } 236 237 /// Read a json file into a JSONValue. 238 unittest { 239 import std.path : buildPath; 240 import std.uuid : randomUUID; 241 import std.file : tempDir, write, mkdirRecurse; 242 243 auto dir = buildPath(tempDir(), "jsonizer_readjson_test"); 244 mkdirRecurse(dir); 245 auto file = buildPath(dir, randomUUID().toString); 246 247 file.write("[1, 2, 3]"); 248 249 auto json = file.readJSON(); 250 251 assert(json.array[0].integer == 1); 252 assert(json.array[1].integer == 2); 253 assert(json.array[2].integer == 3); 254 } 255 256 /// Extract a user-defined class or struct from a JSONValue. 257 /// See `jsonizer.jsonize` for info on how to mark your own types for serialization. 258 T fromJSON(T)(JSONValue json) if (!isBuiltinType!T) { 259 return fromJSONImpl!T(json, null); 260 } 261 262 Inner nestedFromJSON(Inner, Outer)(JSONValue json, Outer outer) { 263 return fromJSONImpl!Inner(json, outer); 264 } 265 266 // Internal implementation of fromJSON for user-defined types 267 // If T is a nested class, pass the parent of type P 268 // otherwise pass null for the parent 269 T fromJSONImpl(T, P)(JSONValue json, P parent = null) if (!isBuiltinType!T) { 270 static if (is(typeof(null) : T)) { 271 if (json.type == JSON_TYPE.NULL) { return null; } 272 } 273 274 // try constructing from a primitive type using a single-param constructor 275 if (json.type != JSON_TYPE.OBJECT) { 276 return invokePrimitiveCtor!T(json, parent); 277 } 278 279 static if (!isNested!T && is(T == class) && is(typeof(T.init.populateFromJSON))) 280 { 281 // look for class keyword in json 282 auto className = json.fromJSON!string(jsonizeClassKeyword, null); 283 // try creating an instance with Object.factory 284 if (className !is null) { 285 auto obj = Object.factory(className); 286 assert(obj !is null, "failed to Object.factory " ~ className); 287 auto instance = cast(T) obj; 288 assert(instance !is null, "failed to cast " ~ className ~ " to " ~ T.stringof); 289 instance.populateFromJSON(json); 290 return instance; 291 } 292 } 293 294 // next, try to find a contructor marked with @jsonize and call that 295 static if (__traits(hasMember, T, "__ctor")) { 296 alias Overloads = TypeTuple!(__traits(getOverloads, T, "__ctor")); 297 foreach(overload ; Overloads) { 298 static if (staticIndexOf!(jsonize, __traits(getAttributes, overload)) >= 0) { 299 if (canSatisfyCtor!overload(json)) { 300 return invokeCustomJsonCtor!(T, overload)(json, parent); 301 } 302 } 303 } 304 305 // no constructor worked, is default-construction an option? 306 static if(!hasDefaultCtor!T) { 307 // not default-constructable, need to fail here 308 alias ctors = Filter!(isJsonized, __traits(getOverloads, T, "__ctor")); 309 JsonizeConstructorException.doThrow!(T, ctors)(json); 310 } 311 } 312 313 // if no @jsonized ctor, try to use a default ctor and populate the fields 314 static if(hasDefaultCtor!T) { 315 return invokeDefaultCtor!(T)(json, parent); 316 } 317 318 assert(0, "all attempts at deserializing " ~ fullyQualifiedName!T ~ " failed."); 319 } 320 321 /// Deserialize an instance of a user-defined type from a json object. 322 unittest { 323 import jsonizer.jsonize; 324 import jsonizer.tojson; 325 326 static struct Foo { 327 mixin JsonizeMe; 328 329 @jsonize { 330 int i; 331 string[] a; 332 } 333 } 334 335 auto jstr = q{ 336 { 337 "i": 1, 338 "a": [ "a", "b" ] 339 } 340 }; 341 342 // you could use `readJSON` instead of `parseJSON.fromJSON` 343 auto foo = jstr.parseJSON.fromJSON!Foo; 344 assert(foo.i == 1); 345 assert(foo.a == [ "a", "b" ]); 346 } 347 348 // return true if keys can satisfy parameter names 349 private bool canSatisfyCtor(alias Ctor)(JSONValue json) { 350 auto obj = json.object; 351 alias Params = ParameterIdentifierTuple!Ctor; 352 alias Types = ParameterTypeTuple!Ctor; 353 alias Defaults = ParameterDefaultValueTuple!Ctor; 354 foreach(i ; staticIota!(0, Params.length)) { 355 if (Params[i] !in obj && typeid(Defaults[i]) == typeid(void)) { 356 return false; // param had no default value and was not specified 357 } 358 } 359 return true; 360 } 361 362 private T invokeCustomJsonCtor(T, alias Ctor, P)(JSONValue json, P parent) { 363 enum params = ParameterIdentifierTuple!(Ctor); 364 alias defaults = ParameterDefaultValueTuple!(Ctor); 365 alias Types = ParameterTypeTuple!(Ctor); 366 Tuple!(Types) args; 367 foreach(i ; staticIota!(0, params.length)) { 368 enum paramName = params[i]; 369 if (paramName in json.object) { 370 args[i] = json.object[paramName].fromJSON!(Types[i]); 371 } 372 else { // no value specified in json 373 static if (is(defaults[i] == void)) { 374 enforce(0, "parameter " ~ paramName ~ " has no default value and was not specified"); 375 } 376 else { 377 args[i] = defaults[i]; 378 } 379 } 380 } 381 static if (isNested!T) { 382 return constructNested!T(parent, args.expand); 383 } 384 else static if (is(T == class)) { 385 return new T(args.expand); 386 } 387 else { 388 return T(args.expand); 389 } 390 } 391 392 private Inner constructNested(Inner, Outer, Args ...)(Outer outer, Args args) { 393 return outer..new Inner(args); 394 } 395 396 private T invokeDefaultCtor(T, P)(JSONValue json, P parent) { 397 T obj; 398 399 static if (isNested!T) { 400 obj = parent..new T; 401 } 402 else static if (is(T == struct)) { 403 obj = T.init; 404 } 405 else { 406 obj = new T; 407 } 408 409 obj.populateFromJSON(json); 410 return obj; 411 } 412 413 private template isJsonized(alias member) { 414 static bool helper() { 415 foreach(attr ; __traits(getAttributes, member)) { 416 static if (is(attr == jsonize) || is(typeof(attr) == jsonize)) { 417 return true; 418 } 419 } 420 return false; 421 } 422 423 enum isJsonized = helper(); 424 } 425 426 private T invokePrimitiveCtor(T, P)(JSONValue json, P parent) { 427 static if (__traits(hasMember, T, "__ctor")) { 428 foreach(overload ; __traits(getOverloads, T, "__ctor")) { 429 alias Types = ParameterTypeTuple!overload; 430 431 // look for an @jsonized ctor with a single parameter 432 static if (hasAttribute!(jsonize, overload) && Types.length == 1) { 433 return construct!T(parent, json.fromJSON!(Types[0])); 434 } 435 } 436 } 437 438 throw new JsonizeTypeException(typeid(T), json, JSON_TYPE.OBJECT); 439 } 440 441 unittest { 442 static class Foo { 443 @jsonize int i; 444 @jsonize("s") string _s; 445 @jsonize @property string sprop() { return _s; } 446 @jsonize @property void sprop(string str) { _s = str; } 447 float f; 448 @property int iprop() { return i; } 449 450 @jsonize this(string s) { } 451 this(int i) { } 452 } 453 454 Foo f; 455 static assert(isJsonized!(__traits(getMember, f, "i"))); 456 static assert(isJsonized!(__traits(getMember, f, "_s"))); 457 static assert(isJsonized!(__traits(getMember, f, "sprop"))); 458 static assert(isJsonized!(__traits(getMember, f, "i"))); 459 static assert(!isJsonized!(__traits(getMember, f, "f"))); 460 static assert(!isJsonized!(__traits(getMember, f, "iprop"))); 461 462 import std.typetuple : Filter; 463 static assert(Filter!(isJsonized, __traits(getOverloads, Foo, "__ctor")).length == 1); 464 }