/
serialization.cr
478 lines (438 loc) · 16.4 KB
/
serialization.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
module JSON
annotation Field
end
# The `JSON::Serializable` module automatically generates methods for JSON serialization when included.
#
# ### Example
#
# ```
# require "json"
#
# class Location
# include JSON::Serializable
#
# @[JSON::Field(key: "lat")]
# property latitude : Float64
#
# @[JSON::Field(key: "lng")]
# property longitude : Float64
# end
#
# class House
# include JSON::Serializable
# property address : String
# property location : Location?
# end
#
# house = House.from_json(%({"address": "Crystal Road 1234", "location": {"lat": 12.3, "lng": 34.5}}))
# house.address # => "Crystal Road 1234"
# house.location # => #<Location:0x10cd93d80 @latitude=12.3, @longitude=34.5>
# house.to_json # => %({"address":"Crystal Road 1234","location":{"lat":12.3,"lng":34.5}})
#
# houses = Array(House).from_json(%([{"address": "Crystal Road 1234", "location": {"lat": 12.3, "lng": 34.5}}]))
# houses.size # => 1
# houses.to_json # => %([{"address":"Crystal Road 1234","location":{"lat":12.3,"lng":34.5}}])
# ```
#
# ### Usage
#
# Including `JSON::Serializable` will create `#to_json` and `self.from_json` methods on the current class,
# and a constructor which takes a `JSON::PullParser`. By default, these methods serialize into a json
# object containing the value of every instance variable, the keys being the instance variable name.
# Most primitives and collections supported as instance variable values (string, integer, array, hash, etc.),
# along with objects which define to_json and a constructor taking a `JSON::PullParser`.
# Union types are also supported, including unions with nil. If multiple types in a union parse correctly,
# it is undefined which one will be chosen.
#
# To change how individual instance variables are parsed and serialized, the annotation `JSON::Field`
# can be placed on the instance variable. Annotating property, getter and setter macros is also allowed.
# ```
# require "json"
#
# class A
# include JSON::Serializable
#
# @[JSON::Field(key: "my_key", emit_null: true)]
# getter a : Int32?
# end
# ```
#
# `JSON::Field` properties:
# * **ignore**: if `true` skip this field in serialization and deserialization (by default false)
# * **ignore_serialize**: if `true` skip this field in serialization (by default false)
# * **ignore_deserialize**: if `true` skip this field in deserialization (by default false)
# * **key**: the value of the key in the json object (by default the name of the instance variable)
# * **root**: assume the value is inside a JSON object with a given key (see `Object.from_json(string_or_io, root)`)
# * **converter**: specify an alternate type for parsing and generation. The converter must define `from_json(JSON::PullParser)` and `to_json(value, JSON::Builder)`. Examples of converters are a `Time::Format` instance and `Time::EpochConverter` for `Time`.
# * **presence**: if `true`, a `@{{key}}_present` instance variable will be generated when the key was present (even if it has a `null` value), `false` by default
# * **emit_null**: if `true`, emits a `null` value for nilable property (by default nulls are not emitted)
#
# Deserialization also respects default values of variables:
# ```
# require "json"
#
# struct A
# include JSON::Serializable
# @a : Int32
# @b : Float64 = 1.0
# end
#
# A.from_json(%<{"a":1}>) # => A(@a=1, @b=1.0)
# ```
#
# ### Extensions: `JSON::Serializable::Strict` and `JSON::Serializable::Unmapped`.
#
# If the `JSON::Serializable::Strict` module is included, unknown properties in the JSON
# document will raise a parse exception. By default the unknown properties
# are silently ignored.
# If the `JSON::Serializable::Unmapped` module is included, unknown properties in the JSON
# document will be stored in a `Hash(String, JSON::Any)`. On serialization, any keys inside json_unmapped
# will be serialized and appended to the current json object.
# ```
# require "json"
#
# struct A
# include JSON::Serializable
# include JSON::Serializable::Unmapped
# @a : Int32
# end
#
# a = A.from_json(%({"a":1,"b":2})) # => A(@json_unmapped={"b" => 2_i64}, @a=1)
# a.to_json # => {"a":1,"b":2}
# ```
#
#
# ### Class annotation `JSON::Serializable::Options`
#
# supported properties:
# * **emit_nulls**: if `true`, emits a `null` value for all nilable properties (by default nulls are not emitted)
#
# ```
# require "json"
#
# @[JSON::Serializable::Options(emit_nulls: true)]
# class A
# include JSON::Serializable
# @a : Int32?
# end
# ```
#
# ### Discriminator field
#
# A very common JSON serialization strategy for handling different objects
# under a same hierarchy is to use a discriminator field. For example in
# [GeoJSON](https://tools.ietf.org/html/rfc7946) each object has a "type"
# field, and the rest of the fields, and their meaning, depend on its value.
#
# You can use `JSON::Serializable.use_json_discriminator` for this use case.
module Serializable
annotation Options
end
macro included
# Define a `new` directly in the included type,
# so it overloads well with other possible initializes
def self.new(pull : ::JSON::PullParser)
new_from_json_pull_parser(pull)
end
private def self.new_from_json_pull_parser(pull : ::JSON::PullParser)
instance = allocate
instance.initialize(__pull_for_json_serializable: pull)
GC.add_finalizer(instance) if instance.responds_to?(:finalize)
instance
end
# When the type is inherited, carry over the `new`
# so it can compete with other possible initializes
macro inherited
def self.new(pull : ::JSON::PullParser)
new_from_json_pull_parser(pull)
end
end
end
def initialize(*, __pull_for_json_serializable pull : ::JSON::PullParser)
{% begin %}
{% properties = {} of Nil => Nil %}
{% for ivar in @type.instance_vars %}
{% ann = ivar.annotation(::JSON::Field) %}
{% unless ann && (ann[:ignore] || ann[:ignore_deserialize]) %}
{%
properties[ivar.id] = {
type: ivar.type,
key: ((ann && ann[:key]) || ivar).id.stringify,
has_default: ivar.has_default_value?,
default: ivar.default_value,
nilable: ivar.type.nilable?,
root: ann && ann[:root],
converter: ann && ann[:converter],
presence: ann && ann[:presence],
}
%}
{% end %}
{% end %}
{% for name, value in properties %}
%var{name} = nil
%found{name} = false
{% end %}
%location = pull.location
begin
pull.read_begin_object
rescue exc : ::JSON::ParseException
raise ::JSON::SerializableError.new(exc.message, self.class.to_s, nil, *%location, exc)
end
until pull.kind.end_object?
%key_location = pull.location
key = pull.read_object_key
case key
{% for name, value in properties %}
when {{value[:key]}}
%found{name} = true
begin
%var{name} =
{% if value[:nilable] || value[:has_default] %} pull.read_null_or { {% end %}
{% if value[:root] %}
pull.on_key!({{value[:root]}}) do
{% end %}
{% if value[:converter] %}
{{value[:converter]}}.from_json(pull)
{% else %}
::Union({{value[:type]}}).new(pull)
{% end %}
{% if value[:root] %}
end
{% end %}
{% if value[:nilable] || value[:has_default] %} } {% end %}
rescue exc : ::JSON::ParseException
raise ::JSON::SerializableError.new(exc.message, self.class.to_s, {{value[:key]}}, *%key_location, exc)
end
{% end %}
else
on_unknown_json_attribute(pull, key, %key_location)
end
end
pull.read_next
{% for name, value in properties %}
{% unless value[:nilable] || value[:has_default] %}
if %var{name}.nil? && !%found{name} && !::Union({{value[:type]}}).nilable?
raise ::JSON::SerializableError.new("Missing JSON attribute: {{value[:key].id}}", self.class.to_s, nil, *%location, nil)
end
{% end %}
{% if value[:nilable] %}
{% if value[:has_default] != nil %}
@{{name}} = %found{name} ? %var{name} : {{value[:default]}}
{% else %}
@{{name}} = %var{name}
{% end %}
{% elsif value[:has_default] %}
if %found{name} && !%var{name}.nil?
@{{name}} = %var{name}
end
{% else %}
@{{name}} = (%var{name}).as({{value[:type]}})
{% end %}
{% if value[:presence] %}
@{{name}}_present = %found{name}
{% end %}
{% end %}
{% end %}
after_initialize
end
protected def after_initialize
end
protected def on_unknown_json_attribute(pull, key, key_location)
pull.skip
end
protected def on_to_json(json : ::JSON::Builder)
end
def to_json(json : ::JSON::Builder)
{% begin %}
{% options = @type.annotation(::JSON::Serializable::Options) %}
{% emit_nulls = options && options[:emit_nulls] %}
{% properties = {} of Nil => Nil %}
{% for ivar in @type.instance_vars %}
{% ann = ivar.annotation(::JSON::Field) %}
{% unless ann && (ann[:ignore] || ann[:ignore_serialize]) %}
{%
properties[ivar.id] = {
type: ivar.type,
key: ((ann && ann[:key]) || ivar).id.stringify,
root: ann && ann[:root],
converter: ann && ann[:converter],
emit_null: (ann && (ann[:emit_null] != nil) ? ann[:emit_null] : emit_nulls),
}
%}
{% end %}
{% end %}
json.object do
{% for name, value in properties %}
_{{name}} = @{{name}}
{% unless value[:emit_null] %}
unless _{{name}}.nil?
{% end %}
json.field({{value[:key]}}) do
{% if value[:root] %}
{% if value[:emit_null] %}
if _{{name}}.nil?
nil.to_json(json)
else
{% end %}
json.object do
json.field({{value[:root]}}) do
{% end %}
{% if value[:converter] %}
if _{{name}}
{{ value[:converter] }}.to_json(_{{name}}, json)
else
nil.to_json(json)
end
{% else %}
_{{name}}.to_json(json)
{% end %}
{% if value[:root] %}
{% if value[:emit_null] %}
end
{% end %}
end
end
{% end %}
end
{% unless value[:emit_null] %}
end
{% end %}
{% end %}
on_to_json(json)
end
{% end %}
end
module Strict
protected def on_unknown_json_attribute(pull, key, key_location)
raise ::JSON::SerializableError.new("Unknown JSON attribute: #{key}", self.class.to_s, nil, *key_location, nil)
end
end
module Unmapped
@[JSON::Field(ignore: true)]
property json_unmapped = Hash(String, JSON::Any).new
protected def on_unknown_json_attribute(pull, key, key_location)
json_unmapped[key] = begin
JSON::Any.new(pull)
rescue exc : ::JSON::ParseException
raise ::JSON::SerializableError.new(exc.message, self.class.to_s, key, *key_location, exc)
end
end
protected def on_to_json(json)
json_unmapped.each do |key, value|
json.field(key) { value.to_json(json) }
end
end
end
# Tells this class to decode JSON by using a field as a discriminator.
#
# - *field* must be the field name to use as a discriminator
# - *mapping* must be a hash or named tuple where each key-value pair
# maps a discriminator value to a class to deserialize
#
# For example:
#
# ```
# require "json"
#
# abstract class Shape
# include JSON::Serializable
#
# use_json_discriminator "type", {point: Point, circle: Circle}
#
# property type : String
# end
#
# class Point < Shape
# property x : Int32
# property y : Int32
# end
#
# class Circle < Shape
# property x : Int32
# property y : Int32
# property radius : Int32
# end
#
# Shape.from_json(%({"type": "point", "x": 1, "y": 2})) # => #<Point:0x10373ae20 @type="point", @x=1, @y=2>
# Shape.from_json(%({"type": "circle", "x": 1, "y": 2, "radius": 3})) # => #<Circle:0x106a4cea0 @type="circle", @x=1, @y=2, @radius=3>
# ```
macro use_json_discriminator(field, mapping)
{% unless mapping.is_a?(HashLiteral) || mapping.is_a?(NamedTupleLiteral) %}
{% mapping.raise "mapping argument must be a HashLiteral or a NamedTupleLiteral, not #{mapping.class_name.id}" %}
{% end %}
def self.new(pull : ::JSON::PullParser)
location = pull.location
discriminator_value = nil
# Try to find the discriminator while also getting the raw
# string value of the parsed JSON, so then we can pass it
# to the final type.
json = String.build do |io|
JSON.build(io) do |builder|
builder.start_object
pull.read_object do |key|
if key == {{field.id.stringify}}
value_kind = pull.kind
case value_kind
when .string?
discriminator_value = pull.string_value
when .int?
discriminator_value = pull.int_value
when .bool?
discriminator_value = pull.bool_value
else
raise ::JSON::SerializableError.new("JSON discriminator field '{{field.id}}' has an invalid value type of #{value_kind.to_s}", to_s, nil, *location, nil)
end
builder.field(key, discriminator_value)
pull.read_next
else
builder.field(key) { pull.read_raw(builder) }
end
end
builder.end_object
end
end
unless discriminator_value
raise ::JSON::SerializableError.new("Missing JSON discriminator field '{{field.id}}'", to_s, nil, *location, nil)
end
case discriminator_value
{% for key, value in mapping %}
{% if mapping.is_a?(NamedTupleLiteral) %}
when {{key.id.stringify}}
{% else %}
{% if key.is_a?(StringLiteral) %}
when {{key}}
{% elsif key.is_a?(NumberLiteral) || key.is_a?(BoolLiteral) %}
when {{key.id}}
{% elsif key.is_a?(Path) %}
when {{key.resolve}}
{% else %}
{% key.raise "mapping keys must be one of StringLiteral, NumberLiteral, BoolLiteral, or Path, not #{key.class_name.id}" %}
{% end %}
{% end %}
{{value.id}}.from_json(json)
{% end %}
else
raise ::JSON::SerializableError.new("Unknown '{{field.id}}' discriminator value: #{discriminator_value.inspect}", to_s, nil, *location, nil)
end
end
end
end
class SerializableError < ParseException
getter klass : String
getter attribute : String?
def initialize(message : String?, @klass : String, @attribute : String?, line_number : Int32, column_number : Int32, cause)
message = String.build do |io|
io << message
io << "\n parsing "
io << klass
if attribute = @attribute
io << '#' << attribute
end
end
super(message, line_number, column_number, cause)
if cause
@line_number, @column_number = cause.location
end
end
end
end