├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── dub.json ├── example ├── .gitignore ├── Makefile ├── dub.json ├── entities.json └── src │ ├── animator.d │ ├── app.d │ ├── component.d │ ├── entity.d │ ├── geometry.d │ └── sprite.d ├── src └── jsonizer │ ├── common.d │ ├── exceptions.d │ ├── fromjson.d │ ├── jsonize.d │ ├── package.d │ └── tojson.d └── tests ├── classes.d ├── structs.d └── types.d /.gitignore: -------------------------------------------------------------------------------- 1 | # dub 2 | .dub/ 3 | dub.selections.json 4 | __test__* 5 | 6 | # docs 7 | docs/ 8 | docs.json 9 | __dummy.html 10 | 11 | # library 12 | *.a 13 | *.lib 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 rcorre 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SETCOMPILER= 2 | ifdef DC 3 | SETCOMPILER="--compiler=$(DC)" 4 | endif 5 | 6 | all: debug 7 | 8 | debug: 9 | @dub build $(SETCOMPILER) --build=debug --quiet 10 | 11 | release: 12 | @dub build $(SETCOMPILER) release --quiet 13 | 14 | test: 15 | @dub test $(SETCOMPILER) 16 | 17 | docs: 18 | @dub build $(SETCOMPILER) --build=ddox --quiet 19 | 20 | clean: 21 | @dub clean 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | jsonizer: D language JSON serializer 2 | === 3 | 4 | The primary purpose of **jsonizer** is to automate the generation of methods 5 | needed to serialize and deserialize user-defined D structs and classes from JSON 6 | data. jsonizer is not a standalone json parser, but rather a convenience layer 7 | on top of `std.json`, allowing you to more easily work with `JSONValue` objects. 8 | 9 | To use jsonizer, the main components you ened to be aware of are 10 | the methods `fromJSON!T` and `toJSON`, the attribute `@jsonize`, and the mixin 11 | template `JsonizeMe`. 12 | 13 | ## Overview 14 | Jsonizer consists of the following modules: 15 | 16 | - `jsonizer.fromjson` 17 | - parse a `T` from a `JSONValue` using `fromJSON!T` 18 | - parse a `T` from a json string using `fromJSONString!T` 19 | - parse a `T` from a json file using `readJSON!T` 20 | - `jsonizer.tojson` 21 | - convert a `T` to a `JSONValue` using `toJSON!T` 22 | - convert a `T` to a json string using `toJSONString!T` 23 | - write a `T` to a json file using `writeJSON!T` 24 | - `jsonizer.jsonize` 25 | - mixin `JsonizeMe` to enable json serialization for a user-defined type 26 | - use `@jsonize` to mark members for serialization 27 | - `jsonizer.all` 28 | - imports `jsonizer.tojson`, `jsonizer.fromjson`, and `jsonizer.jsonize` 29 | 30 | ## fromJSON!T 31 | `fromJSON!T` converts a `JSONValue` into an object of type `T`. 32 | 33 | ```d 34 | import jsonizer.fromjson; 35 | JSONValue json; // lets assume this has some data in it 36 | int i = json.fromJSON!int; 37 | MyEnum e = json.fromJSON!MyEnum; 38 | MyStruct[] s = json.fromJSON!(MyStruct[]); 39 | MyClass[string] c = json.fromJSON!(MyClass[string]); 40 | ``` 41 | 42 | `fromJSON!T` will fail by throwing a `JsonizeTypeException` if the json object's type is not 43 | something it knows how to convert to `T`. 44 | 45 | For primitive types, `fromJSON` leans on the side of flexibility -- for example, 46 | `fromJSON!int` on a json entry of type `string` will try to parse an `int` from 47 | the `string`. 48 | 49 | For user-defined types, you have to do a little work to set up your struct or 50 | class for jsonizer. 51 | 52 | ## @jsonize and JsonizeMe 53 | The simplest way to make your type support json serialization is to mark its 54 | members with the `@jsonize` attribute and have `mixin JsonizeMe;` somewhere in 55 | your type definition. For example: 56 | 57 | ```d 58 | struct S { 59 | mixin JsonizeMe; // this is required to support jsonization 60 | 61 | @jsonize { // public serialized members 62 | int x; 63 | float f; 64 | } 65 | string dontJsonMe; // jsonizer won't touch members not marked with @jsonize 66 | } 67 | ``` 68 | 69 | The above could be deserialized by calling `fromJSON!S` from a json object like: 70 | 71 | ```json 72 | { "x": 5, "f": 1.2 } 73 | ``` 74 | 75 | Your struct could be converted back into a `JSONValue` by calling `toJSON` on an 76 | instance of it. 77 | 78 | jsonizer can do more than just convert public members though: 79 | 80 | ```d 81 | struct S { 82 | mixin JsonizeMe; // this is required to support jsonization 83 | 84 | // jsonize can convert private members too. 85 | // by default, jsonizer looks for a key in the json matching the member name 86 | // you can change this by passing a string to @jsonize 87 | private @jsonize("f") float _f; 88 | 89 | // you can use properties for more complex serialization 90 | // this is useful for converting types that are non-primitive 91 | // but also not defined by you, like std.datetime's Date 92 | private Date _date; 93 | @property @jsonize { 94 | string date() { return dateToString(_date); } 95 | void date(string str) { _date = dateFromString(str); } 96 | } 97 | } 98 | ``` 99 | 100 | Assuming `dateToString` and `dateFromString` are some functions you defined, the 101 | above could be `fromJSON`ed from a json object looking like: 102 | 103 | ```json 104 | { "f": 2.1, "date": "2015-05-01" } 105 | ``` 106 | 107 | The above examples work on both classes and structs provided the following: 108 | 109 | 1. Your type mixes in `JsonizeMe` 110 | 2. Your members are marked with `@jsonize` 111 | 3. Your type has a no-args constructor 112 | 113 | ### Optional members 114 | By default, if a matching json entry is not found for a member marked with `@jsonize`, 115 | deserialization will fail. 116 | If this is not desired for a given member, mark it with `JsonizeIn.opt`. 117 | 118 | ```d 119 | class MyClass { 120 | @jsonize int i; 121 | @jsonize(JsonizeIn.opt) float f; 122 | } 123 | ``` 124 | 125 | In the above example `json.fromJSON!MyClass` will fail if it does not find a key named "i" in the 126 | json object, but will silently ignore the abscence of a key "f". 127 | 128 | Missing non-optional members trigger a `JsonizeMismatchException`, which contains a list of the 129 | missing keys in `missingKeys`: 130 | 131 | ```d 132 | auto ex = collectException!JsonizeMismatchException(`{ "q": 5.0 }`.parseJSON.fromJSON!MyClass); 133 | 134 | assert(ex.missingKeys == [ "i" ]); 135 | ``` 136 | 137 | The way `@jsonize` takes parameters is rather flexible. While I can't condone making your class look 138 | like the below example, it demonstrates the flexibility of `@jsonize`: 139 | 140 | ```d 141 | class TotalMess { 142 | @jsonize(JsonizeIn.opt) { 143 | @jsonize("i") int _i; 144 | @jsonize("f", JsonizeIn.yes) float _f; 145 | @jsonize(JsonizeIn.yes, "s") float _s; 146 | } 147 | } 148 | ``` 149 | As the above shows, parameters may be passed in any order to @jsonize. 150 | 151 | If you want to serialize only non-default (`val != typeof(val).init`) fields, you can use `JsonizeOut` 152 | 153 | ```d 154 | class TotalMess { 155 | @jsonize(JsonizeOut.opt) { 156 | @jsonize("i") int _i; 157 | @jsonize("f", JsonizeOut.yes) float _f; 158 | @jsonize(JsonizeOut.no, "s") float _s; // never serialized, only requred for deserialization 159 | } 160 | } 161 | ``` 162 | 163 | As a shortcut to `JsonizeIn/JsonizeOut`, you can just use `Jsonize`: 164 | 165 | ```d 166 | class TotalMess { 167 | // equivalent to: @jsonize(JsonizeIn.opt, JsonizeOut.opt) 168 | @jsonize(Jsonize.opt) int a; 169 | } 170 | ``` 171 | 172 | 173 | ### Extra Members 174 | If you would like to ensure that every entry in a json object is being 175 | deserialized, you can pass `JsonizeIgnoreExtraKeys.no` to `JsonizeMe`. 176 | In the example below, `fromJSON!S(jobject)` will throw a 177 | `JsonizeMismatchException` than if fields other than `s` and `i` exist in 178 | `jobject`. 179 | 180 | ```d 181 | struct S { 182 | mixin JsonizeMe(JsonizeIgnoreExtraKeys.no); 183 | string s; 184 | int i; 185 | } 186 | ``` 187 | 188 | When a `JsonizeMismatchException` is caught, you can inspect the extra fields by 189 | looking at `extraKeys`: 190 | 191 | ```d 192 | auto ex = collectException!JsonizeMismatchException( 193 | `{ "i": 5, "f": 0.2, "s": "hi"}`.parseJSON.fromJSON!S); 194 | 195 | assert(ex.extraKeys == [ "f" ]); 196 | ``` 197 | 198 | ## Constructors 199 | In some cases, #3 above may not seem so great. What if your type needs to 200 | support serialization but shouldn't have a default constructor? 201 | In this case, you want to `@jsonize` your constructor: 202 | 203 | ```d 204 | class Custom { 205 | mixin JsonizeMe; 206 | 207 | @jsonize this(int i, string s = "hello") { 208 | _i = i; 209 | _s = s; 210 | } 211 | 212 | private: 213 | @jsonize("i") int _i; 214 | @jsonize("s") string _s; 215 | } 216 | ``` 217 | 218 | Given a type `T` with one or more constructors tagged with `@jsonize`, 219 | `fromJSON!T` will try to match the member names and types to a constructor and 220 | invoke that with the corresponding values from the json object. 221 | Parameters with default values are considered optional; if they are not found in 222 | the json, the default value will be used. The above example could be constructed 223 | from json looking like: 224 | 225 | ```json 226 | { "i": 5, "s": "hi" } 227 | ``` 228 | 229 | If "s" were not present, it would be assigned the value "hello". 230 | 231 | Note that while you can `@jsonize` multiple constructors, there should be no 232 | overlap between situations that could satisfy them. If a given json object could 233 | possibly match multiple constructors, jsonizer chooses arbitrarily (it does not 234 | attempt to pick the 'most appropriate' constructor). 235 | 236 | The method of jsonizing your constructor is also useful for types that need to 237 | perform a more complex setup sequence. 238 | 239 | Also note that when using `@jsonize` constructors, mixing in `JsonizeMe` and 240 | marking members with `@jsonize` are only necessary for serialization -- if your 241 | object only needs to support deserialization, marking a constructor is 242 | sufficient. 243 | 244 | If a type has no default (no-args) constructor and jsonizer cannot invoke any constructor marked 245 | with @jsonize, it will throw a `JsonizeConstructorException` which provides info on what 246 | constructors were attempted. 247 | 248 | ### Primitive Constructors 249 | If a type has a constructor marked with @jsonize that takes a single argument, 250 | it can be constructed from a JSONValue of non-object type. For example, the 251 | following struct could be constructed from a json integer: 252 | 253 | ```d 254 | class IntStruct { 255 | mixin JsonizeMe; 256 | int i; 257 | @jsonize this(int i) { this.i = i; } 258 | } 259 | ``` 260 | 261 | ## Factory construction 262 | This is one of the newer and least tested features of jsonizer. 263 | Suppose you have the following classes: 264 | 265 | ```d 266 | module test; 267 | class TestComponent { 268 | mixin JsonizeMe; 269 | @jsonize int c; 270 | } 271 | 272 | class TestCompA : TestComponent { 273 | mixin JsonizeMe; 274 | @jsonize int a; 275 | } 276 | 277 | class TestCompB : TestComponent { 278 | mixin JsonizeMe; 279 | @jsonize string b; 280 | } 281 | ``` 282 | 283 | and the following json: 284 | 285 | ```json 286 | [ 287 | { 288 | "class": "test.TestCompA", 289 | "c": 1, 290 | "a": 5 291 | }, 292 | { 293 | "class": "test.TestCompB", 294 | "c": 2, 295 | "b": "hello" 296 | } 297 | ] 298 | ``` 299 | 300 | Calling `fromJSON!(TestComponent[])` on a `JSONValue` parsed from the above json 301 | string should yield a TestComponent[] of length 2. 302 | While both have the static type `TestComponent`, one is actually a `TestCompA` 303 | and the other is a `TestCompB`, both with their fields appropriately populated. 304 | 305 | Behind the scenes, jsonizer looks for a special key 'class' in the json (chosen 306 | because class is a D keyword and could not be a member of your type). If it 307 | finds this, it calls Object.factory using the specified string. It then calls 308 | `populateFromJSON`, which is a method generated by the `JsonizeMe` mixin. 309 | 310 | For this to work, your type must: 311 | 1. Have a default constructor 312 | 2. mixin JsonizeMe in **every** class in the hierarchy 313 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "jsonizer", 3 | "description" : "Flexible JSON serializer requiring minimal boilerplate", 4 | "copyright" : "Copyright © 2015, rcorre", 5 | "authors" : ["rcorre"], 6 | "license" : "MIT", 7 | "targetType" : "library", 8 | "dependencies" : { 9 | }, 10 | "configurations": [ 11 | { 12 | "name": "library" 13 | }, 14 | { 15 | "name": "unittest", 16 | "importPaths": [ "src", "tests" ], 17 | "sourcePaths": [ "src", "tests" ] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | docs.json 3 | __dummy.html 4 | example 5 | *.o 6 | *.obj 7 | -------------------------------------------------------------------------------- /example/Makefile: -------------------------------------------------------------------------------- 1 | SETCOMPILER= 2 | ifdef DC 3 | SETCOMPILER="--compiler=$(DC)" 4 | endif 5 | 6 | all: debug 7 | 8 | debug: 9 | @dub build $(SETCOMPILER) --build=debug --quiet 10 | 11 | run: 12 | @dub run $(SETCOMPILER) --build=debug --quiet 13 | 14 | release: 15 | @dub build $(SETCOMPILER) release --quiet 16 | 17 | test: 18 | @dub test $(SETCOMPILER) --quiet 19 | 20 | clean: 21 | @dub clean 22 | -------------------------------------------------------------------------------- /example/dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "description": "Example of using jsonizer in an Entity-Component framework", 4 | "copyright": "Copyright © 2015, erethar", 5 | "authors": ["erethar"], 6 | "dependencies": { 7 | "jsonizer": { "path": ".." } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/entities.json: -------------------------------------------------------------------------------- 1 | { 2 | "player": { 3 | "position": { "x": 5, "y": 40 }, 4 | "components": [ 5 | { 6 | "class": "animator.Animator", 7 | "frameTime": 0.033, 8 | "repeat": "loop" 9 | }, 10 | { 11 | "class": "sprite.Sprite", 12 | "textureName": "person", 13 | "depth": 1, 14 | "textureRegion": { 15 | "x": 0, 16 | "y": 0, 17 | "w": 32, 18 | "h": 32 19 | } 20 | } 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/src/animator.d: -------------------------------------------------------------------------------- 1 | module animator; 2 | 3 | import std.string; 4 | import geometry; 5 | import component; 6 | import jsonizer.jsonize : jsonize, JsonizeMe; 7 | 8 | class Animator : Component { 9 | mixin JsonizeMe; 10 | 11 | enum Repeat { 12 | no, 13 | loop, 14 | reverse 15 | } 16 | 17 | @jsonize { 18 | float frameTime; 19 | Repeat repeat; 20 | } 21 | 22 | override string stringify() { 23 | enum fmt = 24 | `Animator Component: 25 | frameTime : %f 26 | repeat : %s`; 27 | return fmt.format(frameTime, repeat); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/src/app.d: -------------------------------------------------------------------------------- 1 | import std.stdio; 2 | import geometry; 3 | import entity; 4 | import component; 5 | import sprite; 6 | import animator; 7 | import jsonizer.fromjson : readJSON; 8 | 9 | private enum fileName = "entities.json"; 10 | 11 | void main() { 12 | auto entities = fileName.readJSON!(Entity[string]); 13 | auto player = entities["player"]; 14 | writefln("Player at position <%d,%d>", player.position.x, player.position.y); 15 | foreach(component ; player.components) { 16 | writeln(component.stringify); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/src/component.d: -------------------------------------------------------------------------------- 1 | module component; 2 | 3 | import jsonizer.jsonize : JsonizeMe; 4 | 5 | abstract class Component { 6 | mixin JsonizeMe; 7 | 8 | // create a string representation of this component, just to show it was populated 9 | string stringify(); 10 | } 11 | -------------------------------------------------------------------------------- /example/src/entity.d: -------------------------------------------------------------------------------- 1 | module entity; 2 | 3 | import geometry; 4 | import component; 5 | import jsonizer.jsonize : jsonize, JsonizeMe; 6 | 7 | class Entity { 8 | mixin JsonizeMe; 9 | 10 | @jsonize { 11 | Vector position; 12 | Component[] components; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/src/geometry.d: -------------------------------------------------------------------------------- 1 | module geometry; 2 | 3 | // try importing the entire jsonizer package 4 | import jsonizer; 5 | 6 | struct Vector { 7 | mixin JsonizeMe; 8 | @jsonize { 9 | int x, y; 10 | } 11 | } 12 | 13 | struct Rect { 14 | mixin JsonizeMe; 15 | @jsonize { 16 | int x, y, w, h; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /example/src/sprite.d: -------------------------------------------------------------------------------- 1 | module sprite; 2 | 3 | import std.string; 4 | import geometry; 5 | import component; 6 | import jsonizer.jsonize; 7 | 8 | class Sprite : Component { 9 | mixin JsonizeMe; 10 | 11 | @jsonize { 12 | string textureName; 13 | int depth; 14 | Rect textureRegion; 15 | } 16 | 17 | override string stringify() { 18 | enum fmt = 19 | `Sprite Component: 20 | textureName : %s 21 | textureRegion : [%d, %d, %d, %d] 22 | depth : %d`; 23 | return fmt.format( 24 | textureName, 25 | textureRegion.x, textureRegion.y, textureRegion.w, textureRegion.h, 26 | depth); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/jsonizer/common.d: -------------------------------------------------------------------------------- 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 | } 204 | -------------------------------------------------------------------------------- /src/jsonizer/exceptions.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Defines the exceptions that Jsonizer may throw. 3 | * 4 | * Authors: rcorre 5 | * License: MIT 6 | * Copyright: Copyright © 2015, rcorre 7 | * Date: 3/24/15 8 | */ 9 | module jsonizer.exceptions; 10 | 11 | import std.json : JSONValue, JSONType; 12 | import std.string : format, join; 13 | import std.traits : ParameterTypeTuple, ParameterIdentifierTuple; 14 | import std.meta : aliasSeqOf; 15 | import std.range : iota; 16 | import std.typetuple : staticMap; 17 | 18 | /// Base class of any exception thrown by `jsonizer`. 19 | class JsonizeException : Exception { 20 | this(string msg) { 21 | super(msg); 22 | } 23 | } 24 | 25 | /// Thrown when `fromJSON` cannot convert a `JSONValue` into the requested type. 26 | class JsonizeTypeException : Exception { 27 | private enum fmt = 28 | "fromJSON!%s expected json type to be one of %s but got json type %s. json input: %s"; 29 | 30 | const { 31 | TypeInfo targetType; /// Type jsonizer was attempting to deserialize to. 32 | JSONValue json; /// The json value that was being deserialized 33 | JSONType[] expected; /// The JSON_TYPEs that would have been acceptable 34 | } 35 | 36 | this(TypeInfo targetType, JSONValue json, JSONType[] expected ...) { 37 | super(fmt.format(targetType, expected, json.type, json)); 38 | 39 | this.targetType = targetType; 40 | this.json = json; 41 | this.expected = expected; 42 | } 43 | } 44 | 45 | unittest { 46 | import std.algorithm : canFind; 47 | 48 | auto json = JSONValue(4.2f); 49 | auto targetType = typeid(bool); 50 | auto expected = [JSONType.true_, JSONType.false_]; 51 | 52 | auto e = new JsonizeTypeException(targetType, json, JSONType.true_, JSONType.false_); 53 | 54 | assert(e.json == json); 55 | assert(e.targetType == targetType); 56 | assert(e.expected == expected); 57 | assert(e.msg.canFind("fromJSON!bool"), 58 | "JsonizeTypeException should report type argument"); 59 | assert(e.msg.canFind("true_") && e.msg.canFind("false_"), 60 | "JsonizeTypeException should report all acceptable json types"); 61 | } 62 | 63 | /// Thrown when the keys of a json object fail to match up with the members of the target type. 64 | class JsonizeMismatchException : JsonizeException { 65 | private enum fmt = 66 | "Failed to deserialize %s.\n" ~ 67 | "Missing non-optional members: %s.\n" ~ 68 | "Extra keys in json: %s.\n"; 69 | 70 | const { 71 | TypeInfo targetType; /// Type jsonizer was attempting to deserialize to. 72 | string[] extraKeys; /// keys present in json that do not match up to a member. 73 | string[] missingKeys; /// non-optional members that were not found in the json. 74 | } 75 | 76 | this(TypeInfo targetType, string[] extraKeys, string[] missingKeys) { 77 | super(fmt.format(targetType, missingKeys, extraKeys)); 78 | 79 | this.targetType = targetType; 80 | this.extraKeys = extraKeys; 81 | this.missingKeys = missingKeys; 82 | } 83 | } 84 | 85 | unittest { 86 | import std.algorithm : all, canFind; 87 | import std.conv : to; 88 | 89 | static class MyClass { } 90 | 91 | auto targetType = typeid(MyClass); 92 | auto extraKeys = [ "who", "what" ]; 93 | auto missingKeys = [ "where" ]; 94 | 95 | auto e = new JsonizeMismatchException(targetType, extraKeys, missingKeys); 96 | assert(e.targetType == targetType); 97 | assert(e.extraKeys == extraKeys); 98 | assert(e.missingKeys == missingKeys); 99 | assert(e.msg.canFind(targetType.to!string), 100 | "JsonizeMismatchException should report type argument"); 101 | assert(extraKeys.all!(x => e.msg.canFind(x)), 102 | "JsonizeTypeException should report all extra keys"); 103 | assert(missingKeys.all!(x => e.msg.canFind(x)), 104 | "JsonizeTypeException should report all missing keys"); 105 | } 106 | 107 | /// Thrown when a type has no default constructor and the custom constructor cannot be fulfilled. 108 | class JsonizeConstructorException : JsonizeException { 109 | private enum fmt = 110 | "%s has no default constructor, and none of the following constructors could be fulfilled: \n" ~ 111 | "%s\n" ~ 112 | "json object:\n %s"; 113 | 114 | const { 115 | TypeInfo targetType; /// Tye type jsonizer was attempting to deserialize to. 116 | JSONValue json; /// The json value that was being deserialized 117 | } 118 | 119 | /// Construct and throw a `JsonizeConstructorException` 120 | /// Params: 121 | /// T = Type being deserialized 122 | /// Ctors = constructors that were attempted 123 | /// json = json object being deserialized 124 | static void doThrow(T, Ctors ...)(JSONValue json) { 125 | static if (Ctors.length > 0) { 126 | auto signatures = [staticMap!(ctorSignature, Ctors)].join("\n"); 127 | } 128 | else { 129 | auto signatures = ""; 130 | } 131 | 132 | throw new JsonizeConstructorException(typeid(T), signatures, json); 133 | } 134 | 135 | private this(TypeInfo targetType, string ctorSignatures, JSONValue json) { 136 | super(fmt.format(targetType, ctorSignatures, json)); 137 | 138 | this.targetType = targetType; 139 | this.json = json; 140 | } 141 | } 142 | 143 | private: 144 | // Represent the function signature of a constructor as a string. 145 | template ctorSignature(alias ctor) { 146 | alias params = ParameterIdentifierTuple!ctor; 147 | alias types = ParameterTypeTuple!ctor; 148 | 149 | // build a string "type1 param1, type2 param2, ..., typeN paramN" 150 | static string paramString() { 151 | string s = ""; 152 | 153 | foreach(i ; aliasSeqOf!(params.length.iota)) { 154 | s ~= types[i].stringof ~ " " ~ params[i]; 155 | 156 | static if (i < params.length - 1) { 157 | s ~= ", "; 158 | } 159 | } 160 | 161 | return s; 162 | } 163 | 164 | enum ctorSignature = "this(%s)".format(paramString); 165 | } 166 | 167 | unittest { 168 | static class Foo { 169 | this(string s, int i, float f) { } 170 | } 171 | 172 | assert(ctorSignature!(Foo.__ctor) == "this(string s, int i, float f)"); 173 | } 174 | -------------------------------------------------------------------------------- /src/jsonizer/fromjson.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains functions for deserializing JSON data. 3 | * 4 | * Authors: rcorre 5 | * License: MIT 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.meta; 15 | import std.range; 16 | import std.traits; 17 | import std.string; 18 | import std.algorithm; 19 | import std.exception; 20 | import std.typecons; 21 | import jsonizer.exceptions; 22 | import jsonizer.common; 23 | 24 | // HACK: this is a hack to allow referencing this particular overload using 25 | // &fromJSON!T in the JsonizeMe mixin 26 | T _fromJSON(T)(JSONValue json, 27 | in ref JsonizeOptions options = JsonizeOptions.defaults) 28 | { 29 | return fromJSON!T(json, options); 30 | } 31 | 32 | /** 33 | * Deserialize json into a value of type `T`. 34 | * 35 | * Params: 36 | * T = Target type. can be any primitive/builtin D type, or any 37 | * user-defined type using the `JsonizeMe` mixin. 38 | * json = `JSONValue` to deserialize. 39 | * options = configures the deserialization behavior. 40 | */ 41 | T fromJSON(T)(JSONValue json, 42 | in ref JsonizeOptions options = JsonizeOptions.defaults) 43 | { 44 | // JSONValue -- identity 45 | static if (is(T == JSONValue)) 46 | return json; 47 | 48 | // enumeration 49 | else static if (is(T == enum)) { 50 | enforceJsonType!T(json, JSONType.string); 51 | return to!T(json.str); 52 | } 53 | 54 | // boolean 55 | else static if (is(T : bool)) { 56 | if (json.type == JSONType.true_) 57 | return true; 58 | else if (json.type == JSONType.false_) 59 | return false; 60 | 61 | // expected 'true' or 'false' 62 | throw new JsonizeTypeException(typeid(bool), json, JSONType.true_, JSONType.false_); 63 | } 64 | 65 | // string 66 | else static if (is(T : string)) { 67 | if (json.type == JSONType.null_) { return null; } 68 | enforceJsonType!T(json, JSONType.string); 69 | return cast(T) json.str; 70 | } 71 | 72 | // numeric 73 | else static if (is(T : real)) { 74 | switch(json.type) { 75 | case JSONType.float_: 76 | return cast(T) json.floating; 77 | case JSONType.integer: 78 | return cast(T) json.integer; 79 | case JSONType.uinteger: 80 | return cast(T) json.uinteger; 81 | case JSONType.string: 82 | enforce(json.str.isNumeric, format("tried to extract %s from json string %s", T.stringof, json.str)); 83 | return to!T(json.str); // try to parse string as int 84 | default: 85 | } 86 | 87 | throw new JsonizeTypeException(typeid(bool), json, 88 | JSONType.float_, JSONType.integer, JSONType.uinteger, JSONType.string); 89 | } 90 | 91 | // array 92 | else static if (isArray!T) { 93 | if (json.type == JSONType.null_) { return T.init; } 94 | enforceJsonType!T(json, JSONType.array); 95 | alias ElementType = ForeachType!T; 96 | T vals; 97 | foreach(idx, val ; json.array) { 98 | static if (isStaticArray!T) { 99 | vals[idx] = val.fromJSON!ElementType(options); 100 | } 101 | else { 102 | vals ~= val.fromJSON!ElementType(options); 103 | } 104 | } 105 | return vals; 106 | } 107 | 108 | // associative array 109 | else static if (isAssociativeArray!T) { 110 | static assert(is(KeyType!T : string), "toJSON requires string keys for associative array"); 111 | if (json.type == JSONType.null_) { return null; } 112 | enforceJsonType!T(json, JSONType.object); 113 | alias ValType = ValueType!T; 114 | T map; 115 | foreach(key, val ; json.object) { 116 | map[key] = fromJSON!ValType(val, options); 117 | } 118 | return map; 119 | } 120 | 121 | // user-defined class or struct 122 | else static if (!isBuiltinType!T) { 123 | return fromJSONImpl!T(json, null, options); 124 | } 125 | 126 | // by the time we get here, we've tried pretty much everything 127 | else { 128 | static assert(0, "fromJSON can't handle a " ~ T.stringof); 129 | } 130 | } 131 | 132 | /// Extract booleans from json values. 133 | unittest { 134 | assert(JSONValue(false).fromJSON!bool == false); 135 | assert(JSONValue(true).fromJSON!bool == true); 136 | } 137 | 138 | /// Extract a string from a json string. 139 | unittest { 140 | assert(JSONValue("asdf").fromJSON!string == "asdf"); 141 | } 142 | 143 | /// Extract various numeric types. 144 | unittest { 145 | assert(JSONValue(1).fromJSON!int == 1); 146 | assert(JSONValue(2u).fromJSON!uint == 2u); 147 | assert(JSONValue(3.0).fromJSON!double == 3.0); 148 | 149 | // fromJSON accepts numeric strings when a numeric conversion is requested 150 | assert(JSONValue("4").fromJSON!long == 4L); 151 | } 152 | 153 | /// Convert a json string into an enum value. 154 | unittest { 155 | enum Category { one, two } 156 | assert(JSONValue("one").fromJSON!Category == Category.one); 157 | } 158 | 159 | /// Convert a json array into an array. 160 | unittest { 161 | auto a = [ 1, 2, 3 ]; 162 | assert(JSONValue(a).fromJSON!(int[]) == a); 163 | } 164 | 165 | /// Convert a json object to an associative array. 166 | unittest { 167 | auto aa = ["a": 1, "b": 2]; 168 | assert(JSONValue(aa).fromJSON!(int[string]) == aa); 169 | } 170 | 171 | /// Convert a json object to a user-defined type. 172 | /// See the docs for `JsonizeMe` for more detailed examples. 173 | unittest { 174 | import jsonizer.jsonize; 175 | static struct MyStruct { 176 | mixin JsonizeMe; 177 | 178 | @jsonize int i; 179 | @jsonize string s; 180 | float f; 181 | } 182 | 183 | auto json = `{ "i": 5, "s": "tally-ho!" }`.parseJSON; 184 | auto val = json.fromJSON!MyStruct; 185 | assert(val.i == 5); 186 | assert(val.s == "tally-ho!"); 187 | } 188 | 189 | /** 190 | * Extract a value from a json object by its key. 191 | * 192 | * Throws if `json` is not of `JSONType.object` or the key does not exist. 193 | * 194 | * Params: 195 | * T = Target type. can be any primitive/builtin D type, or any 196 | * user-defined type using the `JsonizeMe` mixin. 197 | * json = `JSONValue` to deserialize. 198 | * key = key of desired value within the object. 199 | * options = configures the deserialization behavior. 200 | */ 201 | T fromJSON(T)(JSONValue json, 202 | string key, 203 | in ref JsonizeOptions options = JsonizeOptions.defaults) 204 | { 205 | enforceJsonType!T(json, JSONType.object); 206 | enforce(key in json.object, "tried to extract non-existent key " ~ key ~ " from JSONValue"); 207 | return fromJSON!T(json.object[key], options); 208 | } 209 | 210 | /// Directly extract values from an object by their keys. 211 | unittest { 212 | auto aa = ["a": 1, "b": 2]; 213 | auto json = JSONValue(aa); 214 | assert(json.fromJSON!int("a") == 1); 215 | assert(json.fromJSON!ulong("b") == 2L); 216 | } 217 | 218 | /// Deserialize an instance of a user-defined type from a json object. 219 | unittest { 220 | import jsonizer.jsonize; 221 | 222 | static struct Foo { 223 | mixin JsonizeMe; 224 | @jsonize int i; 225 | @jsonize string[] a; 226 | } 227 | 228 | auto foo = `{"i": 12, "a": [ "a", "b" ]}`.parseJSON.fromJSON!Foo; 229 | assert(foo == Foo(12, [ "a", "b" ])); 230 | } 231 | 232 | /** 233 | * Extract a value from a json object by its key. 234 | * 235 | * Throws if `json` is not of `JSONType.object`. 236 | * Return `defaultVal` if the key does not exist. 237 | * 238 | * Params: 239 | * T = Target type. can be any primitive/builtin D type, or any 240 | * user-defined type using the `JsonizeMe` mixin. 241 | * json = `JSONValue` to deserialize. 242 | * key = key of desired value within the object. 243 | * defaultVal = value to return if key is not found 244 | * options = configures the deserialization behavior. 245 | */ 246 | T fromJSON(T)(JSONValue json, 247 | string key, 248 | T defaultVal, 249 | in ref JsonizeOptions options = JsonizeOptions.defaults) 250 | { 251 | enforceJsonType!T(json, JSONType.object); 252 | return (key in json.object) ? fromJSON!T(json.object[key]) : defaultVal; 253 | } 254 | 255 | /// Substitute default values when keys aren't present. 256 | unittest { 257 | auto aa = ["a": 1, "b": 2]; 258 | auto json = JSONValue(aa); 259 | assert(json.fromJSON!int("a", 7) == 1); 260 | assert(json.fromJSON!int("c", 7) == 7); 261 | } 262 | 263 | /* 264 | * Convert a json value in its string representation into a type `T` 265 | * Params: 266 | * T = Target type. can be any primitive/builtin D type, or any 267 | * user-defined type using the `JsonizeMe` mixin. 268 | * json = JSON-formatted string to deserialize. 269 | * options = configures the deserialization behavior. 270 | */ 271 | T fromJSONString(T)(string json, 272 | in ref JsonizeOptions options = JsonizeOptions.defaults) 273 | { 274 | return fromJSON!T(json.parseJSON, options); 275 | } 276 | 277 | /// Use `fromJSONString` to parse from a json `string` rather than a `JSONValue` 278 | unittest { 279 | assert(fromJSONString!(int[])("[1, 2, 3]") == [1, 2, 3]); 280 | } 281 | 282 | /** 283 | * Read a json-constructable object from a file. 284 | * Params: 285 | * T = Target type. can be any primitive/builtin D type, or any 286 | * user-defined type using the `JsonizeMe` mixin. 287 | * path = filesystem path to json file 288 | * options = configures the deserialization behavior. 289 | */ 290 | T readJSON(T)(string path, 291 | in ref JsonizeOptions options = JsonizeOptions.defaults) 292 | { 293 | auto json = parseJSON(readText(path)); 294 | return fromJSON!T(json, options); 295 | } 296 | 297 | /// Read a json file directly into a specified D type. 298 | unittest { 299 | import std.path : buildPath; 300 | import std.uuid : randomUUID; 301 | import std.file : tempDir, write, mkdirRecurse; 302 | 303 | auto dir = buildPath(tempDir(), "jsonizer_readjson_test"); 304 | mkdirRecurse(dir); 305 | auto file = buildPath(dir, randomUUID().toString); 306 | 307 | file.write("[1, 2, 3]"); 308 | 309 | assert(file.readJSON!(int[]) == [ 1, 2, 3 ]); 310 | } 311 | 312 | /** 313 | * Read the contents of a json file directly into a `JSONValue`. 314 | * Params: 315 | * path = filesystem path to json file 316 | */ 317 | auto readJSON(string path) { 318 | return parseJSON(readText(path)); 319 | } 320 | 321 | /// Read a json file into a JSONValue. 322 | unittest { 323 | import std.path : buildPath; 324 | import std.uuid : randomUUID; 325 | import std.file : tempDir, write, mkdirRecurse; 326 | 327 | auto dir = buildPath(tempDir(), "jsonizer_readjson_test"); 328 | mkdirRecurse(dir); 329 | auto file = buildPath(dir, randomUUID().toString); 330 | 331 | file.write("[1, 2, 3]"); 332 | 333 | auto json = file.readJSON(); 334 | 335 | assert(json.array[0].integer == 1); 336 | assert(json.array[1].integer == 2); 337 | assert(json.array[2].integer == 3); 338 | } 339 | 340 | deprecated("use fromJSON instead") { 341 | /// Deprecated: use `fromJSON` instead. 342 | T extract(T)(JSONValue json) { 343 | return json.fromJSON!T; 344 | } 345 | } 346 | 347 | private: 348 | void enforceJsonType(T)(JSONValue json, JSONType[] expected ...) { 349 | if (!expected.canFind(json.type)) { 350 | throw new JsonizeTypeException(typeid(T), json, expected); 351 | } 352 | } 353 | 354 | unittest { 355 | import std.exception : assertThrown, assertNotThrown; 356 | with (JSONType) { 357 | assertThrown!JsonizeTypeException(enforceJsonType!int(JSONValue("hi"), integer, uinteger)); 358 | assertThrown!JsonizeTypeException(enforceJsonType!(bool[string])(JSONValue([ 5 ]), object)); 359 | assertNotThrown(enforceJsonType!int(JSONValue(5), integer, uinteger)); 360 | assertNotThrown(enforceJsonType!(bool[string])(JSONValue(["key": true]), object)); 361 | } 362 | } 363 | 364 | // Internal implementation of fromJSON for user-defined types 365 | // If T is a nested class, pass the parent of type P 366 | // otherwise pass null for the parent 367 | T fromJSONImpl(T, P)(JSONValue json, P parent, in ref JsonizeOptions options) { 368 | static if (is(typeof(null) : T)) { 369 | if (json.type == JSONType.null_) { return null; } 370 | } 371 | 372 | // try constructing from a primitive type using a single-param constructor 373 | if (json.type != JSONType.object) { 374 | return invokePrimitiveCtor!T(json, parent); 375 | } 376 | 377 | static if (!isNested!T && is(T == class)) 378 | { 379 | // if the class is identified in the json, construct an instance of the 380 | // specified type 381 | auto className = json.fromJSON!string(options.classKey, null); 382 | if (options.classMap) { 383 | if(auto tmp = options.classMap(className)) 384 | className = tmp; 385 | } 386 | if (className && className != fullyQualifiedName!T) { 387 | auto handler = className in T._jsonizeCtors; 388 | assert(handler, className ~ " not registered in " ~ T.stringof); 389 | JsonizeOptions newopts = options; 390 | newopts.classKey = null; // don't recursively loop looking up class name 391 | return (*handler)(json, newopts); 392 | } 393 | } 394 | 395 | // next, try to find a contructor marked with @jsonize and call that 396 | static if (__traits(hasMember, T, "__ctor")) { 397 | alias Overloads = AliasSeq!(__traits(getOverloads, T, "__ctor")); 398 | foreach(overload ; Overloads) { 399 | static if (staticIndexOf!(jsonize, __traits(getAttributes, overload)) >= 0) { 400 | if (canSatisfyCtor!overload(json)) { 401 | return invokeCustomJsonCtor!(T, overload)(json, parent); 402 | } 403 | } 404 | } 405 | 406 | // no constructor worked, is default-construction an option? 407 | static if(!hasDefaultCtor!T) { 408 | // not default-constructable, need to fail here 409 | alias ctors = Filter!(isJsonized, __traits(getOverloads, T, "__ctor")); 410 | JsonizeConstructorException.doThrow!(T, ctors)(json); 411 | } 412 | } 413 | 414 | // if no @jsonized ctor, try to use a default ctor and populate the fields 415 | static if(hasDefaultCtor!T) { 416 | return invokeDefaultCtor!T(json, parent, options); 417 | } 418 | 419 | assert(0, "all attempts at deserializing " ~ fullyQualifiedName!T ~ " failed."); 420 | } 421 | 422 | // return true if keys can satisfy parameter names 423 | bool canSatisfyCtor(alias Ctor)(JSONValue json) { 424 | import std.meta : aliasSeqOf; 425 | auto obj = json.object; 426 | alias Params = ParameterIdentifierTuple!Ctor; 427 | alias Types = ParameterTypeTuple!Ctor; 428 | alias Defaults = ParameterDefaultValueTuple!Ctor; 429 | foreach(i ; aliasSeqOf!(Params.length.iota)) { 430 | if (Params[i] !in obj && typeid(Defaults[i]) == typeid(void)) { 431 | return false; // param had no default value and was not specified 432 | } 433 | } 434 | return true; 435 | } 436 | 437 | T invokeCustomJsonCtor(T, alias Ctor, P)(JSONValue json, P parent) { 438 | import std.meta : aliasSeqOf; 439 | enum params = ParameterIdentifierTuple!(Ctor); 440 | alias defaults = ParameterDefaultValueTuple!(Ctor); 441 | alias Types = ParameterTypeTuple!(Ctor); 442 | Tuple!(Types) args; 443 | foreach(i ; aliasSeqOf!(params.length.iota)) { 444 | enum paramName = params[i]; 445 | if (paramName in json.object) { 446 | args[i] = json.object[paramName].fromJSON!(Types[i]); 447 | } 448 | else { // no value specified in json 449 | static if (is(defaults[i] == void)) { 450 | enforce(0, "parameter " ~ paramName ~ " has no default value and was not specified"); 451 | } 452 | else { 453 | args[i] = defaults[i]; 454 | } 455 | } 456 | } 457 | static if (isNested!T) 458 | return parent.new T(args.expand); 459 | else static if (is(T == class)) 460 | return new T(args.expand); 461 | else 462 | return T(args.expand); 463 | } 464 | 465 | T invokeDefaultCtor(T, P)(JSONValue json, P parent, JsonizeOptions options) { 466 | T obj; 467 | 468 | static if (isNested!T) { 469 | obj = parent.new T; 470 | } 471 | else static if (is(T == struct)) { 472 | obj = T.init; 473 | } 474 | else { 475 | obj = new T; 476 | } 477 | 478 | populate(obj, json, options); 479 | return obj; 480 | } 481 | 482 | alias isJsonized(alias member) = hasUDA!(member, jsonize); 483 | 484 | unittest { 485 | static class Foo { 486 | @jsonize int i; 487 | @jsonize("s") string _s; 488 | @jsonize @property string sprop() { return _s; } 489 | @jsonize @property void sprop(string str) { _s = str; } 490 | float f; 491 | @property int iprop() { return i; } 492 | 493 | @jsonize this(string s) { } 494 | this(int i) { } 495 | } 496 | 497 | Foo f; 498 | static assert(isJsonized!(__traits(getMember, f, "i"))); 499 | static assert(isJsonized!(__traits(getMember, f, "_s"))); 500 | static assert(isJsonized!(__traits(getMember, f, "sprop"))); 501 | static assert(isJsonized!(__traits(getMember, f, "i"))); 502 | static assert(!isJsonized!(__traits(getMember, f, "f"))); 503 | static assert(!isJsonized!(__traits(getMember, f, "iprop"))); 504 | 505 | import std.typetuple : Filter; 506 | static assert(Filter!(isJsonized, __traits(getOverloads, Foo, "__ctor")).length == 1); 507 | } 508 | 509 | T invokePrimitiveCtor(T, P)(JSONValue json, P parent) { 510 | static if (__traits(hasMember, T, "__ctor")) { 511 | foreach(overload ; __traits(getOverloads, T, "__ctor")) { 512 | alias Types = ParameterTypeTuple!overload; 513 | 514 | // look for an @jsonized ctor with a single parameter 515 | static if (hasUDA!(overload, jsonize) && Types.length == 1) { 516 | return construct!T(parent, json.fromJSON!(Types[0])); 517 | } 518 | } 519 | } 520 | 521 | assert(0, "No primitive ctor for type " ~ T.stringof); 522 | } 523 | 524 | void populate(T)(ref T obj, JSONValue json, in JsonizeOptions opt) { 525 | string[] missingKeys; 526 | uint fieldsFound = 0; 527 | 528 | foreach (member ; T._membersWithUDA!jsonize) { 529 | string key = jsonKey!(T, member); 530 | 531 | auto required = JsonizeIn.unspecified; 532 | foreach (attr ; T._getUDAs!(member, jsonize)) 533 | if (attr.perform_in != JsonizeIn.unspecified) 534 | required = attr.perform_in; 535 | 536 | if (required == JsonizeIn.no) continue; 537 | 538 | if (auto jsonval = key in json.object) { 539 | ++fieldsFound; 540 | alias MemberType = T._writeMemberType!member; 541 | 542 | static if (!is(MemberType == void)) { 543 | static if (isAggregateType!MemberType && isNested!MemberType) 544 | auto val = fromJSONImpl!Inner(*jsonval, obj, opt); 545 | else { 546 | auto val = fromJSON!MemberType(*jsonval, opt); 547 | } 548 | 549 | obj._writeMember!(MemberType, member)(val); 550 | } 551 | } 552 | else { 553 | if (required == JsonizeIn.yes) missingKeys ~= key; 554 | } 555 | } 556 | 557 | string[] extraKeys; 558 | if (!T._jsonizeIgnoreExtra && fieldsFound < json.object.length) 559 | extraKeys = json.object.keys.filter!(x => x.isUnknownKey!T).array; 560 | 561 | if (missingKeys.length > 0 || extraKeys.length > 0) 562 | throw new JsonizeMismatchException(typeid(T), extraKeys, missingKeys); 563 | } 564 | 565 | bool isUnknownKey(T)(string key) { 566 | foreach (member ; T._membersWithUDA!jsonize) 567 | if (jsonKey!(T, member) == key) 568 | return false; 569 | 570 | return true; 571 | } 572 | 573 | T construct(T, P, Params ...)(P parent, Params params) { 574 | static if (!is(P == typeof(null))) { 575 | return parent.new T(params); 576 | } 577 | else static if (is(typeof(T(params)) == T)) { 578 | return T(params); 579 | } 580 | else static if (is(typeof(new T(params)) == T)) { 581 | return new T(params); 582 | } 583 | else { 584 | static assert(0, "Cannot construct"); 585 | } 586 | } 587 | 588 | unittest { 589 | static struct Foo { 590 | this(int i) { this.i = i; } 591 | int i; 592 | } 593 | 594 | assert(construct!Foo(null).i == 0); 595 | assert(construct!Foo(null, 4).i == 4); 596 | assert(!__traits(compiles, construct!Foo("asd"))); 597 | } 598 | 599 | unittest { 600 | static class Foo { 601 | this(int i) { this.i = i; } 602 | 603 | this(int i, string s) { 604 | this.i = i; 605 | this.s = s; 606 | } 607 | 608 | int i; 609 | string s; 610 | } 611 | 612 | assert(construct!Foo(null, 4).i == 4); 613 | assert(construct!Foo(null, 4, "asdf").s == "asdf"); 614 | assert(!__traits(compiles, construct!Foo("asd"))); 615 | } 616 | 617 | unittest { 618 | class Foo { 619 | class Bar { 620 | int i; 621 | this(int i) { this.i = i; } 622 | } 623 | } 624 | 625 | auto f = new Foo; 626 | assert(construct!(Foo.Bar)(f, 2).i == 2); 627 | } 628 | 629 | template hasDefaultCtor(T) { 630 | static if (isNested!T) { 631 | alias P = typeof(__traits(parent, T).init); 632 | enum hasDefaultCtor = is(typeof(P.init.new T()) == T); 633 | } 634 | else { 635 | enum hasDefaultCtor = is(typeof(T()) == T) || is(typeof(new T()) == T); 636 | } 637 | } 638 | 639 | version(unittest) { 640 | struct S1 { } 641 | struct S2 { this(int i) { } } 642 | struct S3 { @disable this(); } 643 | 644 | class C1 { } 645 | class C2 { this(string s) { } } 646 | class C3 { class Inner { } } 647 | class C4 { class Inner { this(int i); } } 648 | } 649 | 650 | unittest { 651 | static assert( hasDefaultCtor!S1); 652 | static assert( hasDefaultCtor!S2); 653 | static assert(!hasDefaultCtor!S3); 654 | 655 | static assert( hasDefaultCtor!C1); 656 | static assert(!hasDefaultCtor!C2); 657 | 658 | static assert( hasDefaultCtor!C3); 659 | static assert( hasDefaultCtor!(C3.Inner)); 660 | static assert(!hasDefaultCtor!(C4.Inner)); 661 | } 662 | 663 | template hasCustomJsonCtor(T) { 664 | static if (__traits(hasMember, T, "__ctor")) { 665 | alias Overloads = AliasSeq!(__traits(getOverloads, T, "__ctor")); 666 | 667 | enum test(alias fn) = staticIndexOf!(jsonize, __traits(getAttributes, fn)) >= 0; 668 | 669 | enum hasCustomJsonCtor = anySatisfy!(test, Overloads); 670 | } 671 | else { 672 | enum hasCustomJsonCtor = false; 673 | } 674 | } 675 | 676 | unittest { 677 | static struct S1 { } 678 | static struct S2 { this(int i); } 679 | static struct S3 { @jsonize this(int i); } 680 | static struct S4 { this(float f); @jsonize this(int i); } 681 | 682 | static assert(!hasCustomJsonCtor!S1); 683 | static assert(!hasCustomJsonCtor!S2); 684 | static assert( hasCustomJsonCtor!S3); 685 | static assert( hasCustomJsonCtor!S4); 686 | 687 | static class C1 { } 688 | static class C2 { this() {} } 689 | static class C3 { @jsonize this() {} } 690 | static class C4 { @jsonize this(int i); this(float f); } 691 | 692 | static assert(!hasCustomJsonCtor!C1); 693 | static assert(!hasCustomJsonCtor!C2); 694 | static assert( hasCustomJsonCtor!C3); 695 | static assert( hasCustomJsonCtor!C4); 696 | } 697 | -------------------------------------------------------------------------------- /src/jsonizer/jsonize.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Enables marking user-defined types for JSON serialization. 3 | * 4 | * Authors: rcorre 5 | * License: MIT 6 | * Copyright: Copyright © 2015, rcorre 7 | * Date: 3/23/15 8 | */ 9 | module jsonizer.jsonize; 10 | 11 | import jsonizer.common; 12 | 13 | /** 14 | * Enable `fromJSON`/`toJSON` support for the type this is mixed in to. 15 | * 16 | * In order for fields to be (de)serialized, they must be annotated with 17 | * `jsonize` (in addition to having the mixin within the type). 18 | * 19 | * This mixin will _not_ recursively apply to nested types. If a nested type is 20 | * to be serialized, it must have `JsonizeMe` mixed in as well. 21 | * Params: 22 | * ignoreExtra = whether to silently ignore json keys that do not map to serialized members 23 | */ 24 | mixin template JsonizeMe(JsonizeIgnoreExtraKeys ignoreExtra = JsonizeIgnoreExtraKeys.yes) { 25 | static import std.json; 26 | 27 | enum type = typeof(this).stringof; 28 | 29 | template _membersWithUDA(uda) { 30 | import std.meta : Erase, Filter; 31 | import std.traits : isSomeFunction, hasUDA; 32 | import std.string : startsWith; 33 | 34 | template include(string name) { 35 | // filter out inaccessible members, such as those with @disable 36 | static if (__traits(compiles, mixin(type~"."~name))) { 37 | enum isReserved = name.startsWith("__"); 38 | 39 | enum isInstanceField = 40 | __traits(compiles, mixin(type~"."~name~".offsetof")); 41 | 42 | // the &this.name check makes sure this is not an alias 43 | enum isInstanceMethod = 44 | __traits(compiles, mixin("&"~type~"."~name)) && 45 | isSomeFunction!(mixin(type~"."~name)) && 46 | !__traits(isStaticFunction, mixin(type~"."~name)); 47 | 48 | static if ((isInstanceField || isInstanceMethod) && !isReserved) 49 | enum include = hasUDA!(mixin(type~"."~name), uda); 50 | else 51 | enum include = false; 52 | } 53 | else 54 | enum include = false; 55 | } 56 | 57 | enum members = Erase!("this", __traits(allMembers, typeof(this))); 58 | alias _membersWithUDA = Filter!(include, members); 59 | } 60 | 61 | template _getUDAs(string name, alias uda) { 62 | import std.meta : Filter; 63 | import std.traits : getUDAs; 64 | enum isValue(alias T) = is(typeof(T)); 65 | alias _getUDAs = Filter!(isValue, getUDAs!(mixin(type~"."~name), uda)); 66 | } 67 | 68 | template _writeMemberType(string name) { 69 | import std.meta : Filter, AliasSeq; 70 | import std.traits : Parameters; 71 | alias overloads = AliasSeq!(__traits(getOverloads, typeof(this), name)); 72 | enum hasOneArg(alias f) = Parameters!f.length == 1; 73 | alias setters = Filter!(hasOneArg, overloads); 74 | void tryassign()() { mixin("this."~name~"=this."~name~";"); } 75 | 76 | static if (setters.length) 77 | alias _writeMemberType = Parameters!(setters[0]); 78 | else static if (__traits(compiles, tryassign())) 79 | alias _writeMemberType = typeof(mixin(type~"."~name)); 80 | else 81 | alias _writeMemberType = void; 82 | } 83 | 84 | auto _readMember(string name)() { 85 | return __traits(getMember, this, name); 86 | } 87 | 88 | void _writeMember(T, string name)(T val) { 89 | __traits(getMember, this, name) = val; 90 | } 91 | 92 | static import std.json; 93 | static import jsonizer.common; 94 | alias _jsonizeIgnoreExtra = ignoreExtra; 95 | private alias constructor = 96 | typeof(this) function(std.json.JSONValue, 97 | in ref jsonizer.common.JsonizeOptions); 98 | static constructor[string] _jsonizeCtors; 99 | 100 | static if (is(typeof(this) == class)) { 101 | static this() { 102 | import std.traits : BaseClassesTuple, fullyQualifiedName; 103 | import jsonizer.fromjson; 104 | enum name = fullyQualifiedName!(typeof(this)); 105 | foreach (base ; BaseClassesTuple!(typeof(this))) 106 | static if (__traits(hasMember, base, "_jsonizeCtors")) 107 | base._jsonizeCtors[name] = &_fromJSON!(typeof(this)); 108 | } 109 | } 110 | } 111 | 112 | unittest { 113 | static struct attr { string s; } 114 | static struct S { 115 | mixin JsonizeMe; 116 | @attr this(int i) { } 117 | @attr this(this) { } 118 | @attr ~this() { } 119 | @attr int a; 120 | @attr static int b; 121 | @attr void c() { } 122 | @attr static void d() { } 123 | @attr int e(string s) { return 1; } 124 | @attr static int f(string s) { return 1; } 125 | @attr("foo") int g; 126 | @attr("foo") static int h; 127 | int i; 128 | static int j; 129 | void k() { }; 130 | static void l() { }; 131 | alias Int = int; 132 | enum s = 5; 133 | } 134 | 135 | static assert ([S._membersWithUDA!attr] == ["a", "c", "e", "g"]); 136 | } 137 | 138 | unittest { 139 | struct attr { string s; } 140 | struct Outer { 141 | mixin JsonizeMe; 142 | @attr int a; 143 | struct Inner { 144 | mixin JsonizeMe; 145 | @attr this(int i) { } 146 | @attr this(this) { } 147 | @attr int b; 148 | } 149 | } 150 | 151 | static assert ([Outer._membersWithUDA!attr] == ["a"]); 152 | static assert ([Outer.Inner._membersWithUDA!attr] == ["b"]); 153 | } 154 | 155 | unittest { 156 | struct attr { string s; } 157 | struct A { 158 | mixin JsonizeMe; 159 | @disable this(); 160 | @disable this(this); 161 | @attr int a; 162 | } 163 | 164 | static assert ([A._membersWithUDA!attr] == ["a"]); 165 | } 166 | 167 | unittest { 168 | struct attr { string s; } 169 | 170 | static class A { 171 | mixin JsonizeMe; 172 | @attr int a; 173 | @attr string b() { return "hi"; } 174 | string c() { return "hi"; } 175 | } 176 | 177 | static assert ([A._membersWithUDA!attr] == ["a", "b"]); 178 | 179 | static class B : A { mixin JsonizeMe; } 180 | 181 | static assert ([B._membersWithUDA!attr] == ["a", "b"]); 182 | 183 | static class C : A { 184 | mixin JsonizeMe; 185 | @attr int d; 186 | } 187 | 188 | static assert ([C._membersWithUDA!attr] == ["d", "a", "b"]); 189 | 190 | static class D : A { 191 | mixin JsonizeMe; 192 | @disable int a; 193 | } 194 | 195 | static assert ([D._membersWithUDA!attr] == ["b"]); 196 | } 197 | 198 | // Validate name conflicts (issue #36) 199 | unittest { 200 | static struct attr { string s; } 201 | static struct S { 202 | mixin JsonizeMe; 203 | @attr("foo") string name, key; 204 | } 205 | 206 | static assert([S._membersWithUDA!attr] == ["name", "key"]); 207 | static assert([S._getUDAs!("name", attr)] == [attr("foo")]); 208 | static assert([S._getUDAs!("key", attr)] == [attr("foo")]); 209 | } 210 | 211 | // #40: Can't deserialize both as exact type and as part of a hierarchy 212 | unittest 213 | { 214 | import jsonizer.fromjson; 215 | import jsonizer.tojson; 216 | 217 | static class Base 218 | { 219 | mixin JsonizeMe; 220 | @jsonize("class") string className() { return this.classinfo.name; } 221 | } 222 | static class Derived : Base 223 | { 224 | mixin JsonizeMe; 225 | } 226 | 227 | auto a = new Derived(); 228 | auto b = a.toJSON.fromJSON!Derived; 229 | assert(b !is null); 230 | } 231 | -------------------------------------------------------------------------------- /src/jsonizer/package.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Import all public modules for jsonizer. 3 | * 4 | * Authors: rcorre 5 | * License: MIT 6 | * Copyright: Copyright © 2015, rcorre 7 | * Date: 3/23/15 8 | */ 9 | module jsonizer; 10 | 11 | public import jsonizer.tojson; 12 | public import jsonizer.fromjson; 13 | public import jsonizer.jsonize; 14 | public import jsonizer.common; 15 | public import jsonizer.exceptions; 16 | 17 | /// object serialization -- fields only 18 | unittest { 19 | import std.math : approxEqual; 20 | 21 | static class Fields { 22 | // classes must have a no-args constructor 23 | // or a constructor marked with @jsonize (see later example) 24 | this() { } 25 | 26 | this(int iVal, float fVal, string sVal, int[] aVal, string noJson) { 27 | i = iVal; 28 | f = fVal; 29 | s = sVal; 30 | a = aVal; 31 | dontJsonMe = noJson; 32 | } 33 | 34 | mixin JsonizeMe; 35 | 36 | @jsonize { // fields to jsonize -- test different access levels 37 | public int i; 38 | protected float f; 39 | public int[] a; 40 | private string s; 41 | } 42 | string dontJsonMe; 43 | 44 | override bool opEquals(Object o) { 45 | auto other = cast(Fields) o; 46 | return i == other.i && s == other.s && a == other.a && f.approxEqual(other.f); 47 | } 48 | } 49 | 50 | auto obj = new Fields(1, 4.2, "tally ho!", [9, 8, 7, 6], "blarg"); 51 | auto json = toJSON!Fields(obj); 52 | 53 | assert(json.object["i"].integer == 1); 54 | assert(json.object["f"].floating.approxEqual(4.2)); 55 | assert(json.object["s"].str == "tally ho!"); 56 | assert(json.object["a"].array[0].integer == 9); 57 | assert("dontJsonMe" !in json.object); 58 | 59 | // reconstruct from json 60 | auto r = fromJSON!Fields(json); 61 | assert(r.i == 1); 62 | assert(r.f.approxEqual(4.2)); 63 | assert(r.s == "tally ho!"); 64 | assert(r.a == [9, 8, 7, 6]); 65 | assert(r.dontJsonMe is null); 66 | 67 | // array of objects 68 | auto a = [ 69 | new Fields(1, 4.2, "tally ho!", [9, 8, 7, 6], "blarg"), 70 | new Fields(7, 42.2, "yea merrily", [1, 4, 6, 4], "asparagus") 71 | ]; 72 | 73 | // serialize array of user objects to json 74 | auto jsonArray = toJSON!(Fields[])(a); 75 | // reconstruct from json 76 | assert(fromJSON!(Fields[])(jsonArray) == a); 77 | } 78 | 79 | /// object serialization with properties 80 | unittest { 81 | import std.math : approxEqual; 82 | 83 | static class Props { 84 | this() { } // class must have a no-args ctor 85 | 86 | this(int iVal, float fVal, string sVal, string noJson) { 87 | _i = iVal; 88 | _f = fVal; 89 | _s = sVal; 90 | _dontJsonMe = noJson; 91 | } 92 | 93 | mixin JsonizeMe; 94 | 95 | @property { 96 | // jsonize ref property accessing private field 97 | @jsonize ref int i() { return _i; } 98 | // jsonize property with non-trivial get/set methods 99 | @jsonize float f() { return _f - 3; } // the jsonized value will equal _f - 3 100 | float f(float val) { return _f = val + 5; } // 5 will be added to _f when retrieving from json 101 | // don't jsonize these properties 102 | ref string s() { return _s; } 103 | ref string dontJsonMe() { return _dontJsonMe; } 104 | } 105 | 106 | private: 107 | int _i; 108 | float _f; 109 | @jsonize string _s; 110 | string _dontJsonMe; 111 | } 112 | 113 | auto obj = new Props(1, 4.2, "tally ho!", "blarg"); 114 | auto json = toJSON(obj); 115 | 116 | assert(json.object["i"].integer == 1); 117 | assert(json.object["f"].floating.approxEqual(4.2 - 3.0)); // property should have subtracted 3 on retrieval 118 | assert(json.object["_s"].str == "tally ho!"); 119 | assert("dontJsonMe" !in json.object); 120 | 121 | auto r = fromJSON!Props(json); 122 | assert(r.i == 1); 123 | assert(r._f.approxEqual(4.2 - 3.0 + 5.0)); // property accessor should add 5 124 | assert(r._s == "tally ho!"); 125 | assert(r.dontJsonMe is null); 126 | } 127 | 128 | /// object serialization with custom constructor 129 | unittest { 130 | import std.conv : to; 131 | import std.json : parseJSON; 132 | import std.math : approxEqual; 133 | import jsonizer.tojson : toJSON; 134 | 135 | static class Custom { 136 | mixin JsonizeMe; 137 | 138 | this(int i) { 139 | _i = i; 140 | _s = "something"; 141 | _f = 10.2; 142 | } 143 | 144 | @jsonize this(int _i, string _s, float _f = 20.2) { 145 | this._i = _i; 146 | this._s = _s ~ " jsonized"; 147 | this._f = _f; 148 | } 149 | 150 | @jsonize this(double d) { // alternate ctor 151 | _f = d.to!float; 152 | _s = d.to!string; 153 | _i = d.to!int; 154 | } 155 | 156 | private: 157 | @jsonize { 158 | string _s; 159 | float _f; 160 | int _i; 161 | } 162 | } 163 | 164 | auto c = new Custom(12); 165 | auto json = toJSON(c); 166 | assert(json.object["_i"].integer == 12); 167 | assert(json.object["_s"].str == "something"); 168 | assert(json.object["_f"].floating.approxEqual(10.2)); 169 | auto c2 = fromJSON!Custom(json); 170 | assert(c2._i == 12); 171 | assert(c2._s == "something jsonized"); 172 | assert(c2._f.approxEqual(10.2)); 173 | 174 | // test alternate ctor 175 | json = parseJSON(`{"d" : 5}`); 176 | c = json.fromJSON!Custom; 177 | assert(c._f.approxEqual(5) && c._i == 5 && c._s == "5"); 178 | } 179 | 180 | /// struct serialization 181 | unittest { 182 | import std.math : approxEqual; 183 | 184 | static struct S { 185 | mixin JsonizeMe; 186 | 187 | @jsonize { 188 | int x; 189 | float f; 190 | string s; 191 | } 192 | int dontJsonMe; 193 | 194 | this(int x, float f, string s, int noJson) { 195 | this.x = x; 196 | this.f = f; 197 | this.s = s; 198 | this.dontJsonMe = noJson; 199 | } 200 | } 201 | 202 | auto s = S(5, 4.2, "bogus", 7); 203 | auto json = toJSON(s); // serialize a struct 204 | 205 | assert(json.object["x"].integer == 5); 206 | assert(json.object["f"].floating.approxEqual(4.2)); 207 | assert(json.object["s"].str == "bogus"); 208 | assert("dontJsonMe" !in json.object); 209 | 210 | auto r = fromJSON!S(json); 211 | assert(r.x == 5); 212 | assert(r.f.approxEqual(4.2)); 213 | assert(r.s == "bogus"); 214 | assert(r.dontJsonMe == int.init); 215 | } 216 | 217 | /// json file I/O 218 | unittest { 219 | import std.file : remove; 220 | import jsonizer.fromjson : readJSON; 221 | import jsonizer.tojson : writeJSON; 222 | 223 | enum file = "test.json"; 224 | scope(exit) remove(file); 225 | 226 | static struct Data { 227 | mixin JsonizeMe; 228 | 229 | @jsonize { 230 | int x; 231 | string s; 232 | float f; 233 | } 234 | } 235 | 236 | // write an array of user-defined structs 237 | auto array = [Data(5, "preposterous", 12.7), Data(8, "tesseract", -2.7), Data(5, "baby sloths", 102.7)]; 238 | file.writeJSON(array); 239 | auto readBack = file.readJSON!(Data[]); 240 | assert(readBack == array); 241 | 242 | // now try an associative array 243 | auto aa = ["alpha": Data(27, "yams", 0), "gamma": Data(88, "spork", -99.999)]; 244 | file.writeJSON(aa); 245 | auto aaReadBack = file.readJSON!(Data[string]); 246 | assert(aaReadBack == aa); 247 | } 248 | 249 | /// inheritance 250 | unittest { 251 | import std.math : approxEqual; 252 | static class Parent { 253 | mixin JsonizeMe; 254 | @jsonize int x; 255 | @jsonize string s; 256 | } 257 | 258 | static class Child : Parent { 259 | mixin JsonizeMe; 260 | @jsonize float f; 261 | } 262 | 263 | auto c = new Child; 264 | c.x = 5; 265 | c.s = "hello"; 266 | c.f = 2.1; 267 | 268 | auto json = c.toJSON; 269 | assert(json.fromJSON!int("x") == 5); 270 | assert(json.fromJSON!string("s") == "hello"); 271 | assert(json.fromJSON!float("f").approxEqual(2.1)); 272 | 273 | auto child = json.fromJSON!Child; 274 | assert(child !is null); 275 | assert(child.x == 5 && child.s == "hello" && child.f.approxEqual(2.1)); 276 | 277 | auto parent = json.fromJSON!Parent; 278 | assert(parent.x == 5 && parent.s == "hello"); 279 | } 280 | 281 | /// inheritance with ctors 282 | unittest { 283 | import std.math : approxEqual; 284 | static class Parent { 285 | mixin JsonizeMe; 286 | 287 | @jsonize this(int x, string s) { 288 | _x = x; 289 | _s = s; 290 | } 291 | 292 | @jsonize @property { 293 | int x() { return _x; } 294 | string s() { return _s; } 295 | } 296 | 297 | private: 298 | int _x; 299 | string _s; 300 | } 301 | 302 | static class Child : Parent { 303 | mixin JsonizeMe; 304 | 305 | @jsonize this(int x, string s, float f) { 306 | super(x, s); 307 | _f = f; 308 | } 309 | 310 | @jsonize @property { 311 | float f() { return _f; } 312 | } 313 | 314 | private: 315 | float _f; 316 | } 317 | 318 | auto c = new Child(5, "hello", 2.1); 319 | 320 | auto json = c.toJSON; 321 | assert(json.fromJSON!int("x") == 5); 322 | assert(json.fromJSON!string("s") == "hello"); 323 | assert(json.fromJSON!float("f").approxEqual(2.1)); 324 | 325 | auto child = json.fromJSON!Child; 326 | assert(child.x == 5 && child.s == "hello" && child.f.approxEqual(2.1)); 327 | 328 | auto parent = json.fromJSON!Parent; 329 | assert(parent.x == 5 && parent.s == "hello"); 330 | } 331 | 332 | /// renamed members 333 | unittest { 334 | static class Bleh { 335 | mixin JsonizeMe; 336 | private { 337 | @jsonize("x") int _x; 338 | @jsonize("s") string _s; 339 | } 340 | } 341 | 342 | auto b = new Bleh; 343 | b._x = 5; 344 | b._s = "blah"; 345 | 346 | auto json = b.toJSON; 347 | 348 | assert(json.fromJSON!int("x") == 5); 349 | assert(json.fromJSON!string("s") == "blah"); 350 | 351 | auto reconstruct = json.fromJSON!Bleh; 352 | assert(reconstruct._x == b._x && reconstruct._s == b._s); 353 | } 354 | 355 | /// type inference 356 | unittest { 357 | import std.json : parseJSON; 358 | import std.string : format; 359 | import std.traits : fullyQualifiedName; 360 | 361 | static class TestComponent { 362 | mixin JsonizeMe; 363 | @jsonize int c; 364 | } 365 | 366 | static class TestCompA : TestComponent { 367 | mixin JsonizeMe; 368 | @jsonize int a; 369 | } 370 | 371 | static class TestCompB : TestComponent { 372 | mixin JsonizeMe; 373 | @jsonize string b; 374 | } 375 | 376 | string classKeyA = fullyQualifiedName!TestCompA; 377 | string classKeyB = fullyQualifiedName!TestCompB; 378 | 379 | auto data = `[ 380 | { 381 | "class": "%s", 382 | "c": 1, 383 | "a": 5 384 | }, 385 | { 386 | "class": "%s", 387 | "c": 2, 388 | "b": "hello" 389 | } 390 | ]`.format(classKeyA, classKeyB).parseJSON.fromJSON!(TestComponent[]); 391 | 392 | auto a = cast(TestCompA) data[0]; 393 | auto b = cast(TestCompB) data[1]; 394 | 395 | assert(a !is null && a.c == 1 && a.a == 5); 396 | assert(b !is null && b.c == 2 && b.b == "hello"); 397 | } 398 | 399 | /// type inference with custom type key 400 | unittest { 401 | import std.string : format; 402 | import std.traits : fullyQualifiedName; 403 | 404 | static class TestComponent { 405 | mixin JsonizeMe; 406 | @jsonize int c; 407 | } 408 | 409 | static class TestCompA : TestComponent { 410 | mixin JsonizeMe; 411 | @jsonize int a; 412 | } 413 | 414 | static class TestCompB : TestComponent { 415 | mixin JsonizeMe; 416 | @jsonize string b; 417 | } 418 | 419 | // use "type" instead of "class" to identify dynamic type 420 | JsonizeOptions options; 421 | options.classKey = "type"; 422 | 423 | // need to use these because unittest is assigned weird name 424 | // normally would just be "modulename.classname" 425 | string classKeyA = fullyQualifiedName!TestCompA; 426 | string classKeyB = fullyQualifiedName!TestCompB; 427 | 428 | auto data = `[ 429 | { 430 | "type": "%s", 431 | "c": 1, 432 | "a": 5 433 | }, 434 | { 435 | "type": "%s", 436 | "c": 2, 437 | "b": "hello" 438 | } 439 | ]`.format(classKeyA, classKeyB) 440 | .fromJSONString!(TestComponent[])(options); 441 | 442 | auto a = cast(TestCompA) data[0]; 443 | auto b = cast(TestCompB) data[1]; 444 | assert(a !is null && a.c == 1 && a.a == 5); 445 | assert(b !is null && b.c == 2 && b.b == "hello"); 446 | } 447 | 448 | //test the class map 449 | unittest { 450 | import std.string : format; 451 | import std.traits : fullyQualifiedName; 452 | 453 | static class TestComponent { 454 | mixin JsonizeMe; 455 | @jsonize int c; 456 | } 457 | 458 | static class TestCompA : TestComponent { 459 | mixin JsonizeMe; 460 | @jsonize int a; 461 | } 462 | 463 | static class TestCompB : TestComponent { 464 | mixin JsonizeMe; 465 | @jsonize string b; 466 | } 467 | 468 | // use "type" instead of "class" to identify dynamic type 469 | JsonizeOptions options; 470 | options.classKey = "type"; 471 | 472 | const string wrongName = "unrelated"; 473 | 474 | string[string] classMap = [ 475 | TestCompA.stringof : fullyQualifiedName!TestCompA, 476 | TestCompB.stringof : fullyQualifiedName!TestCompB, 477 | wrongName : fullyQualifiedName!TestCompA 478 | ]; 479 | 480 | options.classMap = delegate string(string rawKey) { 481 | if(auto val = rawKey in classMap) 482 | return *val; 483 | else 484 | return null; 485 | }; 486 | 487 | auto data = `[ 488 | { 489 | "type": "%s", 490 | "c": 1, 491 | "a": 5 492 | }, 493 | { 494 | "type": "%s", 495 | "c": 2, 496 | "b": "hello" 497 | }, 498 | { 499 | "type": "%s", 500 | "c": 3, 501 | "a": 12 502 | } 503 | ]`.format(TestCompA.stringof, TestCompB.stringof, wrongName) 504 | .fromJSONString!(TestComponent[])(options); 505 | 506 | auto a = cast(TestCompA) data[0]; 507 | auto b = cast(TestCompB) data[1]; 508 | auto c = cast(TestCompA) data[2]; 509 | assert(a !is null && a.c == 1 && a.a == 5); 510 | assert(b !is null && b.c == 2 && b.b == "hello"); 511 | assert(c !is null && c.c == 3 && c.a == 12); 512 | } 513 | 514 | // Validate issue #20: 515 | // Unable to de-jsonize a class when a construct is marked @jsonize. 516 | unittest { 517 | import std.json : parseJSON; 518 | import std.algorithm : canFind; 519 | import std.exception : collectException; 520 | import jsonizer.common : jsonize; 521 | import jsonizer.exceptions : JsonizeConstructorException; 522 | import jsonizer.fromjson : fromJSON; 523 | import jsonizer.jsonize : JsonizeMe; 524 | 525 | static class A { 526 | mixin JsonizeMe; 527 | private const int a; 528 | 529 | this(float f) { 530 | a = 0; 531 | } 532 | 533 | @jsonize this(int a) { 534 | this.a = a; 535 | } 536 | 537 | @jsonize this(string s, float f) { 538 | a = 0; 539 | } 540 | } 541 | 542 | auto ex = collectException!JsonizeConstructorException(`{}`.parseJSON.fromJSON!A); 543 | assert(ex !is null, "failure to match @jsonize'd constructors should throw"); 544 | assert(ex.msg.canFind("(int a)") && ex.msg.canFind("(string s, float f)"), 545 | "JsonizeConstructorException message should contain attempted constructors"); 546 | assert(!ex.msg.canFind("(float f)"), 547 | "JsonizeConstructorException message should not contain non-jsonized constructors"); 548 | } 549 | 550 | // Validate issue #17: 551 | // Unable to construct class containing private (not marked with @jsonize) types. 552 | unittest { 553 | import std.json : parseJSON; 554 | import jsonizer; 555 | 556 | static class A { 557 | mixin JsonizeMe; 558 | 559 | private int a; 560 | 561 | @jsonize public this(int a) { 562 | this.a = a; 563 | } 564 | } 565 | 566 | auto json = `{ "a": 5}`.parseJSON; 567 | auto a = fromJSON!A(json); 568 | 569 | assert(a.a == 5); 570 | } 571 | 572 | // Validate issue #18: 573 | // Unable to construct class with const types. 574 | unittest { 575 | import std.json : parseJSON; 576 | import jsonizer; 577 | 578 | static class A { 579 | mixin JsonizeMe; 580 | 581 | const int a; 582 | 583 | @jsonize public this(int a) { 584 | this.a = a; 585 | } 586 | } 587 | 588 | auto json = `{ "a": 5}`.parseJSON; 589 | auto a = fromJSON!A(json); 590 | 591 | assert(a.a == 5); 592 | } 593 | 594 | // Validate issue #19: 595 | // Unable to construct class containing private (not marked with @jsonize) types. 596 | unittest { 597 | import std.json : parseJSON; 598 | import jsonizer; 599 | 600 | static class A { 601 | mixin JsonizeMe; 602 | 603 | alias Integer = int; 604 | Integer a; 605 | 606 | @jsonize public this(Integer a) { 607 | this.a = a; 608 | } 609 | } 610 | 611 | auto json = `{ "a": 5}`.parseJSON; 612 | auto a = fromJSON!A(json); 613 | 614 | assert(a.a == 5); 615 | } 616 | 617 | unittest { 618 | import std.json : parseJSON; 619 | import jsonizer; 620 | 621 | static struct A 622 | { 623 | mixin JsonizeMe; 624 | @jsonize int a; 625 | @jsonize(Jsonize.opt) string attr; 626 | @jsonize(JsonizeIn.opt) string attr2; 627 | } 628 | 629 | auto a = A(5); 630 | assert(a == a.toJSON.fromJSON!A); 631 | assert(a.toJSON == `{ "a":5, "attr2":"" }`.parseJSON); 632 | assert(a.toJSON != `{ "a":5, "attr":"", "attr2":"" }`.parseJSON); 633 | a.attr = "hello"; 634 | assert(a == a.toJSON.fromJSON!A); 635 | assert(a.toJSON == `{ "a":5, "attr":"hello", "attr2":"" }`.parseJSON); 636 | a.attr2 = "world"; 637 | assert(a == a.toJSON.fromJSON!A); 638 | assert(a.toJSON == `{ "a":5, "attr":"hello", "attr2":"world" }`.parseJSON); 639 | a.attr = ""; 640 | assert(a == a.toJSON.fromJSON!A); 641 | assert(a.toJSON == `{ "a":5, "attr2":"world" }`.parseJSON); 642 | } 643 | 644 | unittest { 645 | import std.json : parseJSON; 646 | import jsonizer; 647 | 648 | static struct A 649 | { 650 | mixin JsonizeMe; 651 | @jsonize int a; 652 | @disable int opEquals( ref const(A) ); 653 | } 654 | 655 | static assert(!is(typeof(A.init==A.init))); 656 | 657 | static struct B 658 | { 659 | mixin JsonizeMe; 660 | @jsonize(Jsonize.opt) A a; 661 | } 662 | 663 | auto b = B(A(10)); 664 | assert(b.a.a == 10); 665 | assert(b.a.a == (b.toJSON.fromJSON!B).a.a); 666 | assert(b.toJSON == `{"a":{"a":10}}`.parseJSON); 667 | b.a.a = 0; 668 | assert(b.a.a == int.init ); 669 | assert(b.a.a == (b.toJSON.fromJSON!B).a.a); 670 | assert(b.toJSON == "{}".parseJSON); 671 | } 672 | 673 | unittest { 674 | import std.json : parseJSON; 675 | import std.exception; 676 | import jsonizer; 677 | 678 | static struct T 679 | { 680 | mixin JsonizeMe; 681 | @jsonize(Jsonize.opt) 682 | { 683 | int a; 684 | @jsonize(JsonizeOut.no,JsonizeIn.yes) string b; 685 | @jsonize(JsonizeOut.yes,JsonizeIn.no) string c; 686 | } 687 | } 688 | 689 | auto t = T(5); 690 | assertThrown(t.toJSON.fromJSON!T); 691 | assert(t.toJSON == `{ "a":5, "c":"" }`.parseJSON); 692 | t.b = "hello"; 693 | assert(t == `{ "a":5, "b":"hello" }`.parseJSON.fromJSON!T); 694 | t.c = "world"; 695 | assert(t.toJSON == `{ "a":5, "c":"world" }`.parseJSON); 696 | t.a = 0; 697 | assert(t.toJSON == `{ "c":"world" }`.parseJSON); 698 | auto t2 = `{ "b":"hello", "c":"okda" }`.parseJSON.fromJSON!T; 699 | assert(t.a == t2.a); 700 | assert(t.b == t2.b); 701 | assert(t2.c == ""); 702 | } 703 | 704 | // issue 29: Can't have static fields in JsonizeMe type; doesn't compile 705 | unittest { 706 | import std.json : parseJSON; 707 | static class A { 708 | mixin JsonizeMe; 709 | static string s; 710 | static string str() { return "s"; } 711 | @jsonize int a; 712 | } 713 | auto a = new A; 714 | a.a = 5; 715 | assert(a.toJSON == `{ "a":5 }`.parseJSON); 716 | } 717 | -------------------------------------------------------------------------------- /src/jsonizer/tojson.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Contains functions for serializing JSON data. 3 | * 4 | * Authors: rcorre 5 | * License: MIT 6 | * Copyright: Copyright © 2015, rcorre 7 | * Date: 3/23/15 8 | */ 9 | module jsonizer.tojson; 10 | 11 | import std.json; 12 | import std.traits; 13 | import std.conv; 14 | import std.file; 15 | import std.exception; 16 | import std.typecons; 17 | import jsonizer.common; 18 | 19 | /// convert a JSONValue to a JSONValue (identity) 20 | JSONValue toJSON(JSONValue val) { 21 | return val; 22 | } 23 | 24 | /// convert a bool to a JSONValue 25 | JSONValue toJSON(T : bool)(T val) { 26 | return JSONValue(val); 27 | } 28 | 29 | /// Serialize a boolean. 30 | unittest { 31 | assert(false.toJSON == JSONValue(false)); 32 | assert(true.toJSON == JSONValue(true)); 33 | } 34 | 35 | /// convert a string to a JSONValue 36 | JSONValue toJSON(T : string)(T val) { 37 | return JSONValue(val); 38 | } 39 | 40 | /// Serialize a string. 41 | unittest { 42 | assert("bork".toJSON == JSONValue("bork")); 43 | } 44 | 45 | /// convert a floating point value to a JSONValue 46 | JSONValue toJSON(T : real)(T val) if (!is(T == enum)) { 47 | return JSONValue(val); 48 | } 49 | 50 | /// Serialize a floating-point value. 51 | unittest { 52 | assert(4.1f.toJSON == JSONValue(4.1f)); 53 | } 54 | 55 | /// convert a signed integer to a JSONValue 56 | JSONValue toJSON(T : long)(T val) if (isSigned!T && !is(T == enum)) { 57 | return JSONValue(val); 58 | } 59 | 60 | /// Serialize a signed integer. 61 | unittest { 62 | auto j3 = toJSON(41); 63 | assert(4.toJSON == JSONValue(4)); 64 | assert(4L.toJSON == JSONValue(4L)); 65 | } 66 | 67 | /// convert an unsigned integer to a JSONValue 68 | JSONValue toJSON(T : ulong)(T val) if (isUnsigned!T && !is(T == enum)) { 69 | return JSONValue(val); 70 | } 71 | 72 | /// Serialize an unsigned integer. 73 | unittest { 74 | assert(41u.toJSON == JSONValue(41u)); 75 | } 76 | 77 | /// convert an enum name to a JSONValue 78 | JSONValue toJSON(T)(T val) if (is(T == enum)) { 79 | JSONValue json; 80 | json.str = to!string(val); 81 | return json; 82 | } 83 | 84 | /// Enums are serialized by name. 85 | unittest { 86 | enum Category { one, two } 87 | 88 | assert(Category.one.toJSON.str == "one"); 89 | assert(Category.two.toJSON.str == "two"); 90 | } 91 | 92 | /// convert a homogenous array into a JSONValue array 93 | JSONValue toJSON(T)(T args) if (isArray!T && !isSomeString!T) { 94 | static if (isDynamicArray!T) { 95 | if (args is null) { return JSONValue(null); } 96 | } 97 | JSONValue[] jsonVals; 98 | foreach(arg ; args) { 99 | jsonVals ~= toJSON(arg); 100 | } 101 | JSONValue json; 102 | json.array = jsonVals; 103 | return json; 104 | } 105 | 106 | /// Serialize a homogenous array. 107 | unittest { 108 | auto json = [1, 2, 3].toJSON; 109 | assert(json.type == JSONType.array); 110 | assert(json.array[0].integer == 1); 111 | assert(json.array[1].integer == 2); 112 | assert(json.array[2].integer == 3); 113 | } 114 | 115 | /// convert a set of heterogenous values into a JSONValue array 116 | JSONValue toJSON(T...)(T args) { 117 | JSONValue[] jsonVals; 118 | foreach(arg ; args) { 119 | jsonVals ~= toJSON(arg); 120 | } 121 | JSONValue json; 122 | json.array = jsonVals; 123 | return json; 124 | } 125 | 126 | /// Serialize a heterogenous array. 127 | unittest { 128 | auto json = toJSON(1, "hi", 0.4); 129 | assert(json.type == JSONType.array); 130 | assert(json.array[0].integer == 1); 131 | assert(json.array[1].str == "hi"); 132 | assert(json.array[2].floating == 0.4); 133 | } 134 | 135 | /// convert a associative array into a JSONValue object 136 | JSONValue toJSON(T)(T map) if (isAssociativeArray!T) { 137 | assert(is(KeyType!T : string), "toJSON requires string keys for associative array"); 138 | if (map is null) { return JSONValue(null); } 139 | JSONValue[string] obj; 140 | foreach(key, val ; map) { 141 | obj[key] = toJSON(val); 142 | } 143 | JSONValue json; 144 | json.object = obj; 145 | return json; 146 | } 147 | 148 | /// Serialize an associative array. 149 | unittest { 150 | auto json = ["a" : 1, "b" : 2, "c" : 3].toJSON; 151 | assert(json.type == JSONType.object); 152 | assert(json.object["a"].integer == 1); 153 | assert(json.object["b"].integer == 2); 154 | assert(json.object["c"].integer == 3); 155 | } 156 | 157 | /// Convert a user-defined type to json. 158 | /// See `jsonizer.jsonize` for info on how to mark your own types for serialization. 159 | JSONValue toJSON(T)(T obj) if (!isBuiltinType!T) { 160 | static if (is (T == class)) 161 | if (obj is null) { return JSONValue(null); } 162 | 163 | JSONValue[string] keyValPairs; 164 | 165 | foreach(member ; T._membersWithUDA!jsonize) { 166 | enum key = jsonKey!(T, member); 167 | 168 | auto output = JsonizeOut.unspecified; 169 | foreach (attr ; T._getUDAs!(member, jsonize)) 170 | if (attr.perform_out != JsonizeOut.unspecified) 171 | output = attr.perform_out; 172 | 173 | if (output == JsonizeOut.no) continue; 174 | 175 | auto val = obj._readMember!member; 176 | if (output == JsonizeOut.opt && isInitial(val)) continue; 177 | 178 | keyValPairs[key] = toJSON(val); 179 | } 180 | 181 | std.json.JSONValue json; 182 | json.object = keyValPairs; 183 | return json; 184 | } 185 | 186 | /// Serialize an instance of a user-defined type to a json object. 187 | unittest { 188 | import jsonizer.jsonize; 189 | 190 | static struct Foo { 191 | mixin JsonizeMe; 192 | @jsonize int i; 193 | @jsonize string[] a; 194 | } 195 | 196 | auto foo = Foo(12, [ "a", "b" ]); 197 | assert(foo.toJSON() == `{"i": 12, "a": [ "a", "b" ]}`.parseJSON); 198 | } 199 | 200 | /// Whether to nicely format json string. 201 | alias PrettyJson = Flag!"PrettyJson"; 202 | 203 | /// Convert an instance of some type `T` directly into a json-formatted string. 204 | /// Params: 205 | /// T = type of object to convert 206 | /// obj = object to convert to sjon 207 | /// pretty = whether to prettify string output 208 | string toJSONString(T)(T obj, PrettyJson pretty = PrettyJson.yes) { 209 | auto json = obj.toJSON!T; 210 | return pretty ? json.toPrettyString : json.toString; 211 | } 212 | 213 | unittest { 214 | assert([1, 2, 3].toJSONString(PrettyJson.no) == "[1,2,3]"); 215 | assert([1, 2, 3].toJSONString(PrettyJson.yes) == "[\n 1,\n 2,\n 3\n]"); 216 | } 217 | 218 | /// Write a jsonizeable object to a file. 219 | /// Params: 220 | /// path = filesystem path to write json to 221 | /// obj = object to convert to json and write to path 222 | void writeJSON(T)(string path, T obj) { 223 | auto json = toJSON!T(obj); 224 | path.write(json.toPrettyString); 225 | } 226 | 227 | unittest { 228 | import std.path : buildPath; 229 | import std.uuid : randomUUID; 230 | 231 | auto dir = buildPath(tempDir(), "jsonizer_writejson_test"); 232 | mkdirRecurse(dir); 233 | auto file = buildPath(dir, randomUUID().toString); 234 | 235 | file.writeJSON([1, 2, 3]); 236 | 237 | auto json = file.readText.parseJSON; 238 | assert(json.array[0].integer == 1); 239 | assert(json.array[1].integer == 2); 240 | assert(json.array[2].integer == 3); 241 | } 242 | 243 | // True if `val` is equal to the initial value of that type 244 | bool isInitial(T)(T val) 245 | { 246 | static if (is(typeof(val == T.init))) 247 | return val == T.init; 248 | else static if (is(typeof(val is T.init))) 249 | return val is T.init; 250 | else static assert(0, "isInitial can't handle " ~ T.stringof); 251 | } 252 | -------------------------------------------------------------------------------- /tests/classes.d: -------------------------------------------------------------------------------- 1 | module tests.classes; 2 | 3 | import tests.types; 4 | import jsonizer; 5 | 6 | // assert that an object can be serialized to JSON and then reconstructed 7 | void runTest(T)(T obj) { 8 | // check JSONValue serialization 9 | assert(obj.toJSON.fromJSON!T == obj); 10 | // check JSON string serialization 11 | assert(obj.toJSONString.fromJSONString!T == obj); 12 | } 13 | 14 | /++ 15 | unittest { 16 | auto obj = new OuterClass; 17 | obj.outerVal = 5; 18 | obj.inner = obj.new InnerClass; 19 | obj.inner.innerVal = 10; 20 | runTest(obj); 21 | } 22 | 23 | unittest { 24 | auto obj = new OuterClassCtor; 25 | obj.outerVal = 5; 26 | obj.inner = obj.new InnerClass(10); 27 | runTest(obj); 28 | } 29 | 30 | unittest { 31 | assert("null".fromJSONString!SimpleClass is null); 32 | } 33 | ++/ 34 | -------------------------------------------------------------------------------- /tests/structs.d: -------------------------------------------------------------------------------- 1 | import std.json; 2 | 3 | import tests.types; 4 | import jsonizer.fromjson; 5 | import jsonizer.tojson; 6 | 7 | // assert that an object can be serialized to JSON and then reconstructed 8 | void runTest(T, Params ...)(Params params) { 9 | T obj = T(params); 10 | // check JSONValue serialization 11 | assert(obj.toJSON.fromJSON!T == obj); 12 | // check JSON string serialization 13 | assert(obj.toJSONString.fromJSONString!T == obj); 14 | } 15 | 16 | // helper to make an empty instance of an array type T 17 | template emptyArray(T) { 18 | enum emptyArray = cast(T) []; 19 | } 20 | 21 | auto staticArray(T, Params ...)(Params params) { 22 | return cast(T[Params.length]) [params]; 23 | } 24 | 25 | /++ 26 | unittest { 27 | runTest!PrimitiveStruct(1, 2UL, true, 0.4f, 0.8, "wat?"); // general case 28 | runTest!PrimitiveStruct(0, 40, false, 0.4f, 22.8, ""); // empty string 29 | runTest!PrimitiveStruct(0, 40, false, 0.4f, 22.8, null); // null string 30 | // NaN and Inf are currently not handled by std.json 31 | //runTest!PrimitiveStruct(0, 40, false, float.nan, 22.8, null); 32 | } 33 | 34 | unittest { 35 | runTest!PrivateFieldsStruct(1, 2UL, true, 0.4f, 0.8, "wat?"); 36 | } 37 | 38 | unittest { 39 | runTest!PropertyStruct(4, false, -2.7f, "asdf"); 40 | } 41 | 42 | unittest { 43 | runTest!ArrayStruct([1, 2, 3], ["a", "b", "c"]); // populated arrays 44 | runTest!ArrayStruct([1, 2, 3], ["a", null, "c"]); // null in array 45 | runTest!ArrayStruct(emptyArray!(int[]), emptyArray!(string[])); // empty arrays 46 | runTest!ArrayStruct(null, null); // null arrays 47 | } 48 | 49 | unittest { 50 | runTest!NestedArrayStruct([[1, 2], [3, 4]], [["a", "b"], ["c", "d"]]); // nested arrays 51 | runTest!NestedArrayStruct([null, [3, 4]], cast(string[][]) [null]); // null entries 52 | runTest!NestedArrayStruct(emptyArray!(int[][]), emptyArray!(string[][])); // empty arrays 53 | runTest!NestedArrayStruct(null, null); // null arrays 54 | } 55 | 56 | unittest { 57 | runTest!StaticArrayStruct(staticArray!int(1, 2, 3), staticArray!string("a", "b")); 58 | } 59 | 60 | unittest { 61 | runTest!NestedStruct(5, "ra", 4.2, [2, 3]); 62 | } 63 | 64 | unittest { 65 | auto json = q{{ "inner": { "i": 1 } }}; 66 | auto nested = json.fromJSONString!Nested2; 67 | assert(nested.inner.i == 1); 68 | } 69 | 70 | unittest { 71 | runTest!AliasedTypeStruct([2, 3]); 72 | } 73 | 74 | unittest { 75 | runTest!CustomCtorStruct(1, 4.2f); 76 | } 77 | 78 | unittest { 79 | runTest!(GenericStruct!int)(5); 80 | runTest!(GenericStruct!string)("s"); 81 | runTest!(GenericStruct!PrimitiveStruct)(PrimitiveStruct(1, 2UL, true, 0.4f, 0.8, "wat?")); 82 | } 83 | 84 | unittest { 85 | auto jstr = "5"; 86 | assert(jstr.fromJSONString!IntStruct == IntStruct(5)); 87 | } 88 | 89 | unittest { 90 | assert(`{"i": 5, "j": 7}`.fromJSONString!JSONValueStruct == 91 | JSONValueStruct(5, JSONValue(7))); 92 | assert(`{"i": 5, "j": "hi"}`.fromJSONString!JSONValueStruct == 93 | JSONValueStruct(5, JSONValue("hi"))); 94 | } 95 | ++/ 96 | -------------------------------------------------------------------------------- /tests/types.d: -------------------------------------------------------------------------------- 1 | /// Types defined for unit testing. 2 | module tests.types; 3 | 4 | import std.json; 5 | import jsonizer.jsonize; 6 | 7 | version (unittest) { 8 | } 9 | else { 10 | static assert(0, "Do not include tests dir unless in unit-test mode!"); 11 | } 12 | 13 | /++ 14 | struct PrimitiveStruct { 15 | mixin JsonizeMe; 16 | 17 | @jsonize { 18 | int i; 19 | ulong l; 20 | bool b; 21 | float f; 22 | double d; 23 | string s; 24 | } 25 | } 26 | 27 | struct PrivateFieldsStruct { 28 | mixin JsonizeMe; 29 | 30 | @jsonize private { 31 | int i; 32 | ulong l; 33 | bool b; 34 | float f; 35 | double d; 36 | string s; 37 | } 38 | } 39 | 40 | struct PropertyStruct { 41 | mixin JsonizeMe; 42 | 43 | @jsonize @property { 44 | // getters 45 | auto i() { return _i; } 46 | auto b() { return _b; } 47 | auto f() { return _f; } 48 | auto s() { return _s; } 49 | 50 | // setters 51 | void i(int val) { _i = val; } 52 | void b(bool val) { _b = val; } 53 | void f(float val) { _f = val; } 54 | void s(string val) { _s = val; } 55 | } 56 | 57 | private: 58 | int _i; 59 | bool _b; 60 | float _f; 61 | string _s; 62 | } 63 | 64 | struct ArrayStruct { 65 | mixin JsonizeMe; 66 | 67 | @jsonize { 68 | int[] i; 69 | string[] s; 70 | } 71 | } 72 | 73 | struct NestedArrayStruct { 74 | mixin JsonizeMe; 75 | 76 | @jsonize { 77 | int[][] i; 78 | string[][] s; 79 | } 80 | } 81 | 82 | struct StaticArrayStruct { 83 | mixin JsonizeMe; 84 | 85 | @jsonize { 86 | int[3] i; 87 | string[2] s; 88 | } 89 | 90 | bool opEquals(StaticArrayStruct other) { 91 | return i == other.i && s == other.s; 92 | } 93 | } 94 | 95 | struct NestedStruct { 96 | mixin JsonizeMe; 97 | @jsonize { 98 | int i; 99 | string s; 100 | Inner inner; 101 | } 102 | 103 | private struct Inner { 104 | mixin JsonizeMe; 105 | @jsonize { 106 | double d; 107 | int[] a; 108 | } 109 | } 110 | 111 | this(int i, string s, double d, int[] a) { 112 | this.i = i; 113 | this.s = s; 114 | inner = Inner(d, a); 115 | } 116 | } 117 | 118 | struct Nested2 { 119 | mixin JsonizeMe; 120 | @jsonize Inner inner; 121 | 122 | private struct Inner { 123 | mixin JsonizeMe; 124 | @jsonize int i; 125 | } 126 | } 127 | 128 | struct AliasedTypeStruct { 129 | mixin JsonizeMe; 130 | alias Ints = int[]; 131 | @jsonize Ints i; 132 | } 133 | 134 | struct CustomCtorStruct { 135 | mixin JsonizeMe; 136 | 137 | @disable this(); 138 | 139 | @jsonize { 140 | int i; 141 | float f; 142 | 143 | this(int i, float f) { 144 | this.i = i; 145 | this.f = f; 146 | } 147 | } 148 | } 149 | 150 | struct JSONValueStruct { 151 | mixin JsonizeMe; 152 | 153 | @jsonize { 154 | int i; 155 | JSONValue j; 156 | } 157 | } 158 | 159 | class SimpleClass { 160 | mixin JsonizeMe; 161 | @jsonize { 162 | int i; 163 | string s; 164 | } 165 | } 166 | 167 | class OuterClass { 168 | mixin JsonizeMe; 169 | 170 | @jsonize { 171 | int outerVal; 172 | InnerClass inner; 173 | } 174 | 175 | class InnerClass { 176 | mixin JsonizeMe; 177 | @jsonize int innerVal; 178 | 179 | override bool opEquals(Object obj) { 180 | auto other = cast(InnerClass) obj; 181 | return other !is null && this.innerVal == other.innerVal; 182 | } 183 | } 184 | 185 | override bool opEquals(Object obj) { 186 | auto other = cast(OuterClass) obj; 187 | return other !is null && this.outerVal == other.outerVal; 188 | } 189 | } 190 | 191 | class OuterClassCtor { 192 | mixin JsonizeMe; 193 | 194 | @jsonize { 195 | int outerVal; 196 | InnerClass inner; 197 | } 198 | 199 | class InnerClass { 200 | mixin JsonizeMe; 201 | @jsonize int i; 202 | 203 | @jsonize this(int i) { this.i = i; } 204 | } 205 | 206 | override bool opEquals(Object obj) { 207 | auto other = cast(OuterClassCtor) obj; 208 | return other !is null && outerVal == other.outerVal && inner.i == other.inner.i; 209 | } 210 | } 211 | 212 | struct GenericStruct(T) { 213 | mixin JsonizeMe; 214 | 215 | @jsonize T val; 216 | } 217 | 218 | struct IntStruct { 219 | mixin JsonizeMe; 220 | int i; 221 | @jsonize this(int i) { this.i = i; } 222 | } 223 | ++/ 224 | --------------------------------------------------------------------------------