1 module json.serialization; 2 3 // this module is not ready for use just yet 4 version( none ): 5 6 private{ 7 import json.parser : parseJson; 8 import json.value; 9 10 import std.meta; 11 import std.string; 12 import std.traits; 13 import std.typecons; 14 15 void ensureNotNull( T )( JsonValue value ) 16 { 17 if( value.isNull ) 18 throw new JsonException( "cannot convert null to %s".format( T.stringof ) ); 19 } 20 21 void ensureDeserializable( T, string name )() 22 { 23 enum canConvert = is( typeof( { 24 auto _ = &JsonValue.opCast!T; 25 } ) ); 26 27 static assert( 28 canConvert, 29 "cannot convert to type %s for field %s".format( T.stringof, name ) 30 ); 31 } 32 33 template isNullable( T, bool typeconsNullable ) 34 { 35 static if( !typeconsNullable ) 36 enum isNullable = is( typeof( { T _ = null; } ) ); 37 else 38 { 39 enum isNullable = is( typeof( { 40 import std.algorithm.searching : startsWith; 41 42 enum name = fullyQualifiedName!( Unqual!T ); 43 static assert( name.startsWith( "std.typecons.Nullable" ) ); 44 } ) ); 45 } 46 } 47 48 enum typeIsJsonObject( T, bool hasDeserializer = true, bool hasSerializer = true ) = is( typeof( { 49 static if( hasDeserializer ) 50 T.fromJson( JsonValue.init ); 51 52 static if( hasSerializer ) 53 JsonValue _ = T.init.toJson(); 54 } ) ); 55 56 enum typeIsJsonValue( T ) = is( typeof( { 57 auto _ = JsonValue( T.init ); 58 } ) ); 59 60 enum isRefNullable( T ) = is( typeof( { 61 import std.algorithm.searching : startsWith; 62 63 enum name = fullyQualifiedName!( Unqual!T ); 64 static assert( name.startsWith( "std.typecons.NullableRef" ) ); 65 } ) ); 66 67 void deserialize( string name, T )( JsonValue value, T* member ) 68 if( isNullable!( T, false ) && !typeIsJsonObject!( T, true, false ) ) 69 { 70 ensureDeserializable!( T, name ); 71 *member = value.isNull ? null : value.to!T; 72 } 73 74 void deserialize( string name, T )( JsonValue value, T* member ) 75 if( typeIsJsonObject!( T, true, false ) ) 76 { 77 *member = value.isNull ? null : T.fromJson( value ); 78 } 79 80 void deserialize( string name, T )( JsonValue value, T* member ) 81 if( isNullable!( T, true ) && !isRefNullable!T ) 82 { 83 alias TDest = Unqual!( ReturnType!( &(*member).get ) ); 84 ensureDeserializable!( TDest, name ); 85 86 *member = value.isNull ? T.init : T( value.to!TDest ); 87 } 88 89 void deserialize( string name, T )( JsonValue value, T* member ) 90 if( isNullable!( T, true ) && isRefNullable!T ) 91 { 92 alias TDest = Unqual!( ReturnType!( &(*member).get ) ); 93 ensureDeserializable!( TDest, name ); 94 95 TDest* ptr = void; 96 97 if( value.isNull ) 98 { 99 *member = T.init; 100 return; 101 } 102 103 *ptr = value.to!TDest; 104 *member = T( ptr ); 105 } 106 107 void deserialize( string name, T )( JsonValue value, T* member ) 108 if( ( !isNullable!( T, true ) && !isNullable!( T, false ) ) && typeIsJsonObject!( T, true, false ) ) 109 { 110 value.ensureNotNull!T; 111 *member = T.fromJson( value ); 112 } 113 114 void deserialize( string name, T )( JsonValue value, T* member ) 115 if( ( !isNullable!( T, true ) && !isNullable!( T, false ) ) && !typeIsJsonObject!( T, true, false ) ) 116 { 117 ensureDeserializable!( T, name ); 118 value.ensureNotNull!T; 119 *member = value.to!T; 120 } 121 } 122 123 enum Required 124 { 125 no, 126 allowNull, 127 disallowNull, 128 always, 129 } 130 131 enum SerializationMode 132 { 133 optIn, 134 optOut, 135 } 136 137 struct JsonSerialization 138 { 139 immutable SerializationMode mode; 140 141 this() @disable; 142 143 this( SerializationMode mode ) 144 { 145 this.mode = mode; 146 } 147 } 148 149 struct JsonIgnore { } 150 151 struct JsonProperty 152 { 153 private string _name; 154 auto name() const @property { return this._name; } 155 156 immutable Required required; 157 158 this( string name, Required required = Required.no ) 159 { 160 this._name = name; 161 this.required = required; 162 } 163 } 164 165 private template Reflector( This ) if( is( This == class ) || is( This == struct ) ) 166 { 167 SerializationMode serializationMode() 168 { 169 static if( hasUDA!( This, JsonSerialization ) ) 170 return getUDAs!( This, JsonSerialization )[0].mode; 171 else 172 return SerializationMode.optOut; 173 } 174 175 enum isJsonIgnored( T... ) = hasUDA!( __traits( getMember, This, T[0] ), JsonIgnore ); 176 enum isJsonProperty( T... ) = hasUDA!( __traits( getMember, This, T[0] ), JsonProperty ); 177 178 template shouldInclude( T... ) 179 { 180 static if( serializationMode == SerializationMode.optIn ) 181 enum shouldInclude = isJsonProperty!T; 182 else static if( serializationMode == SerializationMode.optOut ) 183 enum shouldInclude = !isJsonIgnored!T; 184 else 185 static assert( false, "programmer forgot to implement something" ); 186 } 187 188 enum isImmutable( T... ) = is( typeof( __traits( getMember, This, T[0] ) ) == immutable ) 189 || is( typeof( __traits( getMember, This, T[0] ) ) == const ); 190 191 template not( alias x ) 192 { 193 enum not( T... ) = !x!T; 194 } 195 196 JsonProperty getProperty( alias name )() 197 { 198 static if( isJsonProperty!name ) 199 return getUDAs!( __traits( getMember, This, name ), JsonProperty )[0]; 200 else 201 static assert( false, "'%s' is not a json property".formaT( name ) ); 202 } 203 204 string fieldName( alias name )() 205 { 206 static if( isJsonProperty!name ) 207 { 208 enum prop = getProperty!name; 209 return prop.name !is null && prop.name.length ? prop.name : name; 210 } 211 else return name; 212 } 213 214 Required getRequirement( alias name )() 215 { 216 static if( isJsonProperty!name ) 217 return getProperty!( name ).required; 218 else 219 return Required.no; 220 } 221 222 static if( ( FieldNameTuple!This ).length ) 223 enum members = Filter!( not!isImmutable, Filter!( shouldInclude, FieldNameTuple!This ) ); 224 else 225 enum members = AliasSeq!(); 226 } 227 228 mixin template JsonObject() 229 { 230 static assert( 231 is( typeof( this ) == class ) || is( typeof( this ) == struct ), 232 "JsonObject template can only be mixed in to a class or struct" 233 ); 234 235 T toJsonString( T = string )( bool indented = false ) if( isSomeString!T ) 236 { 237 return this.toJson().toJsonString( indented ); 238 } 239 240 JsonValue toJson() 241 { 242 alias This = typeof( this ); 243 alias reflector = Reflector!This; 244 245 void ensureConversion( T, string name )() 246 { 247 enum canConvert = is( typeof( { 248 auto _ = JsonValue( T.init ); 249 } ) ); 250 251 static assert( 252 canConvert, 253 "cannot convert from type %s for field %s".format( T.stringof, name ) 254 ); 255 } 256 257 auto json = JsonValue.newObject(); 258 259 foreach( i, name; reflector.members ) 260 { 261 enum field = reflector.fieldName!name; 262 alias type = typeof( __traits( getMember, this, name ) ); 263 264 try 265 { 266 static if( isNullable!type ) 267 { 268 static if( is( typeof( { type _ = null; } ) ) && !typeIsJsonObject!type ) 269 { 270 ensureConversion!( type, name ); 271 mixin( "json[\"%s\"] = this.%s.isNull ? jnull : JsonValue( this );".format( field, name ) ); 272 } 273 else static if( typeIsJsonObject!( type, true ) ) 274 mixin( "json[\"%s\"] = this.%2$s.isNull ? jnull : this.%2$s.toJson();".format( field, name ) ); 275 else // field is Nullable!T or NullableRef!T 276 { 277 alias dest = Unqual!( typeof( __traits( getMember, mixin( "instance." ~ name ), "get" ) ) ); 278 ensureConversion!( dest, name ); 279 280 static if( !isRef!type ) 281 mixin( "instance.%s = value.isNull ? type.init : type( value.to!dest );".format( name ) ); 282 else 283 { 284 dest* ptr = value.isNull ? null : new dest( value.to!dest ); 285 mixin( "instance.%s = value.isNull ? type.init : type( ptr );".format( name ) ); 286 } 287 } 288 } 289 else // field is not nullable 290 { 291 if( value.isNull ) 292 throw new JsonException( "cannot convert null to %s".format( type.stringof ) ); 293 294 static if( typeIsJsonObject!( type, true ) ) 295 mixin( "instanse.%s = type.fromJson( value );".format( name ) ); 296 else 297 { 298 ensureConversion!( type, name ); 299 mixin( "instance.%s = value.to!type;".format( name ) ); 300 } 301 } 302 } 303 catch( JsonException ex ) 304 { 305 throw new JsonException( 306 "error deserializing field '%s': %s".format( field, ex.msg ), 307 __FILE__, 308 __LINE__, 309 ex 310 ); 311 } 312 } 313 314 return json; 315 } 316 317 static typeof( this ) fromJson( S )( S str ) if( isSomeString!S ) 318 { 319 auto json = str.parseJson(); 320 return fromJson( json ); 321 } 322 323 static typeof( this ) fromJson( JsonValue json ) 324 { 325 if( !json.isObject ) 326 throw new JsonException( "JSON value is not an object" ); 327 328 alias This = typeof( this ); 329 alias reflector = Reflector!This; 330 331 auto instance = { 332 static if( is( This == struct ) ) 333 return This(); 334 else static if( is( This == class ) ) 335 return new This(); 336 else 337 static assert( false, "programmer messed up template constraints" ); 338 }(); 339 340 foreach( i, name; reflector.members ) 341 { 342 enum field = reflector.fieldName!name; 343 enum required = reflector.getRequirement!name; 344 alias type = typeof( __traits( getMember, instance, name ) ); 345 346 immutable exists = json.hasKey( field ); 347 348 if( !exists && ( required == Required.always || required == Required.allowNull ) ) 349 throw new JsonException( "missing required field '%s'".format( field ) ); 350 351 if( !exists ) continue; 352 353 auto value = json[field]; 354 if( value.isNull && ( required == Required.always || required == Required.disallowNull ) ) 355 throw new JsonException( "value for '%s' cannot be null".format( field ) ); 356 357 try 358 { 359 type* member = &__traits( getMember, instance, name ); 360 value.deserialize!( field )( member ); 361 } 362 catch( JsonException ex ) 363 { 364 throw new JsonException( 365 "error deserializing field '%s': %s".format( field, ex.msg ), 366 __FILE__, 367 __LINE__, 368 ex 369 ); 370 } 371 } 372 373 return instance; 374 } 375 } 376 377 version( unittest ) 378 { 379 private class Invalid { } 380 381 @JsonSerialization( SerializationMode.optIn ) 382 private struct Person 383 { 384 private { 385 @JsonProperty( "first_name", Required.always ) 386 string _first; 387 388 @JsonProperty( "last_name", Required.always ) 389 string _last; 390 391 @JsonProperty( "age", Required.always ) 392 ubyte _age; 393 394 @JsonIgnore 395 Invalid _invalid; 396 } 397 398 string firstName() @property { return _first; } 399 string lastName() @property { return _last; } 400 ubyte age() @property { return _age; } 401 402 mixin JsonObject; 403 } 404 } 405 406 unittest 407 { 408 auto text = q{{ 409 "first_name": "John", 410 "last_name": "Doe", 411 "age": 35 412 }}; 413 414 auto john = Person.fromJson( text ); 415 416 assert( john.firstName == "John" ); 417 assert( john.lastName == "Doe" ); 418 assert( john.age == 35 ); 419 }