1 module jsonizer.common; 2 3 /// use @jsonize to mark members to be (de)serialized from/to json 4 /// use @jsonize to mark a single contructor to use when creating an object using extract 5 /// use @jsonize("name") to make a member use the json key "name" 6 /// use @jsonize(Jsonize.[yes/opt]) to choose whether the parameter is optional 7 /// use @jsonize(JsonizeIn.[yes/opt/no]) to choose whether the parameter is optional for deserialization 8 /// use @jsonize(JsonizeOut.[yes/opt/no]) to choose whether the parameter is optional for serialization 9 struct jsonize { 10 /// alternate name used to identify member in json 11 string key; 12 13 /// whether member is required during deserialization 14 JsonizeIn perform_in = JsonizeIn.unspecified; 15 /// whether serialized member 16 JsonizeOut perform_out = JsonizeOut.unspecified; 17 18 /// parameters to @jsonize may be specified in any order 19 /// valid uses of @jsonize include: 20 /// @jsonize 21 /// @jsonize("foo") 22 /// @jsonize(Jsonize.opt) 23 /// @jsonize("bar", Jsonize.opt) 24 /// @jsonize(Jsonize.opt, "bar") 25 this(T ...)(T params) { 26 foreach(idx , param ; params) { 27 alias type = T[idx]; 28 static if (is(type == Jsonize)) { 29 perform_in = cast(JsonizeIn)param; 30 perform_out = cast(JsonizeOut)param; 31 } 32 else static if (is(type == JsonizeIn)) { 33 perform_in = param; 34 } 35 else static if (is(type == JsonizeOut)) { 36 perform_out = param; 37 } 38 else static if (is(type : string)) { 39 key = param; 40 } 41 else { 42 assert(0, "invalid @jsonize parameter of type " ~ typeid(type)); 43 } 44 } 45 } 46 } 47 48 /// Control the strictness with which a field is deserialized 49 enum JsonizeIn 50 { 51 /// The default. Equivalent to `yes` unless overridden by another UDA. 52 unspecified = 0, 53 /// always deserialize this field, fail if it is not present 54 yes = 1, 55 /// deserialize if found, but continue without error if it is missing 56 opt = 2, 57 /// never deserialize this field 58 no = 3 59 } 60 61 /// Control the strictness with which a field is serialized 62 enum JsonizeOut 63 { 64 /// the default value -- equivalent to `yes` 65 unspecified = 0, 66 /// always serialize this field 67 yes = 1, 68 /// serialize only if it not equal to the initial value of the type 69 opt = 2, 70 /// never serialize this field 71 no = 3 72 } 73 74 /// Shortcut for setting both `JsonizeIn` and `JsonizeOut` 75 enum Jsonize 76 { 77 /// equivalent to JsonizeIn.yes, JsonizeOut.yes 78 yes = 1, 79 /// equivalent to JsonizeIn.opt, JsonizeOut.opt 80 opt = 2 81 } 82 83 /// Use of `Jsonize(In,Out)`: 84 unittest { 85 import std.json : parseJSON; 86 import std.exception : collectException, assertNotThrown; 87 import jsonizer.jsonize : JsonizeMe; 88 import jsonizer.fromjson : fromJSON; 89 import jsonizer.exceptions : JsonizeMismatchException; 90 static struct S { 91 mixin JsonizeMe; 92 93 @jsonize { 94 int i; // i is non-opt (default) 95 @jsonize(Jsonize.opt) { 96 @jsonize("_s") string s; // s is optional 97 @jsonize(Jsonize.yes) float f; // f is non-optional (overrides outer attribute) 98 } 99 } 100 } 101 102 assertNotThrown(`{ "i": 5, "f": 0.2}`.parseJSON.fromJSON!S); 103 auto ex = collectException!JsonizeMismatchException(`{ "i": 5 }`.parseJSON.fromJSON!S); 104 105 assert(ex !is null, "missing non-optional field 'f' should trigger JsonizeMismatchException"); 106 assert(ex.targetType == typeid(S)); 107 assert(ex.missingKeys == [ "f" ]); 108 assert(ex.extraKeys == [ ]); 109 } 110 111 /// Whether to silently ignore json keys that do not map to serialized members. 112 enum JsonizeIgnoreExtraKeys { 113 no, /// silently ignore extra keys in the json object being deserialized 114 yes /// fail if the json object contains a keys that does not map to a serialized field 115 } 116 117 /// Use of `JsonizeIgnoreExtraKeys`: 118 unittest { 119 import std.json : parseJSON; 120 import std.exception : collectException, assertNotThrown; 121 import jsonizer.jsonize : JsonizeMe; 122 import jsonizer.fromjson : fromJSON; 123 import jsonizer.exceptions : JsonizeMismatchException; 124 125 static struct NoCares { 126 mixin JsonizeMe; 127 @jsonize { 128 int i; 129 float f; 130 } 131 } 132 133 static struct VeryStrict { 134 mixin JsonizeMe!(JsonizeIgnoreExtraKeys.no); 135 @jsonize { 136 int i; 137 float f; 138 } 139 } 140 141 // no extra fields, neither should throw 142 assertNotThrown(`{ "i": 5, "f": 0.2}`.parseJSON.fromJSON!NoCares); 143 assertNotThrown(`{ "i": 5, "f": 0.2}`.parseJSON.fromJSON!VeryStrict); 144 145 // extra field "s" 146 // `NoCares` ignores extra keys, so it will not throw 147 assertNotThrown(`{ "i": 5, "f": 0.2, "s": "hi"}`.parseJSON.fromJSON!NoCares); 148 // `VeryStrict` does not ignore extra keys 149 auto ex = collectException!JsonizeMismatchException( 150 `{ "i": 5, "f": 0.2, "s": "hi"}`.parseJSON.fromJSON!VeryStrict); 151 152 assert(ex !is null, "extra field 's' should trigger JsonizeMismatchException"); 153 assert(ex.targetType == typeid(VeryStrict)); 154 assert(ex.missingKeys == [ ]); 155 assert(ex.extraKeys == [ "s" ]); 156 } 157 158 /// Customize the behavior of `toJSON` and `fromJSON`. 159 struct JsonizeOptions { 160 /** 161 * A default-constructed `JsonizeOptions`. 162 * Used implicilty if no explicit options are given to `fromJSON` or `toJSON`. 163 */ 164 static immutable defaults = JsonizeOptions.init; 165 166 /** 167 * The key of a field identifying the D type of a json object. 168 * 169 * If this key is found in the json object, `fromJSON` will try to factory 170 * construct an object of the type identified. 171 * 172 * This is useful when deserializing a collection of some type `T`, where the 173 * actual instances may be different subtypes of `T`. 174 * 175 * Setting `classKey` to null will disable factory construction. 176 */ 177 string classKey = "class"; 178 179 /** 180 * A function to attempt identifier remapping from the name found under `classKey`. 181 * 182 * If this function is provided, then when the `classKey` is found, this function 183 * will attempt to remap the value. This function should return either the fully 184 * qualified class name or null. Returned non-null values indicate that the 185 * remapping has succeeded. A null value will indicate the mapping has failed 186 * and the original value will be used in the object factory. 187 * 188 * This is particularly useful when input JSON has not originated from D. 189 */ 190 string delegate(string) classMap; 191 } 192 193 package: 194 // Get the json key corresponding to `T.member`. 195 template jsonKey(T, string member) { 196 alias attrs = T._getUDAs!(member, jsonize); 197 static if (!attrs.length) 198 enum jsonKey = member; 199 else static if (attrs[$ - 1].key) 200 enum jsonKey = attrs[$ - 1].key; 201 else 202 enum jsonKey = member; 203 }