Open HertzDevil opened 2 years ago
Is it possible to annotate the constants directly?
class Point
include JSON::Serializable
@[JSON::Constant(key: "type")]
TYPE = "point"
property x : Int32
property y : Int32
end
There is no way to access annotations on constants, as their TypeNode
s are inaccessible in the macro language.
Anyway, this is a brilliant idea. I hope crystal could get it soon.
i dont think this is good, because, in 1 project i have hundreds classes, and generate use_json_discriminator in macro automatically, but with this i need to type such things all the time.
@[JSON::Serializable::Options(constants: {type: "point"})]
class Point < Shape; ...; end
@[JSON::Serializable::Options(constants: {type: "circle"})]
class Circle < Shape; ...; end
You can attach annotations by reopening the classes too. In particular you can reopen the current class with something like:
class Foo
{% begin %}
@[JSON::Serializable::Options(...)]
class ::{{ @type }}
end
{% end %}
end
So if you can use a macro to generate the use_*_discriminator
call, you can use a macro to generate those Options
annotations.
(In fact I wonder if this issue can be implemented on top of use_*_discriminator
this way)
i do it like this:
abstract class Bla
macro finished
use_json_discriminator("type", {
{% for subclass in @type.all_subclasses %}
{{subclass.name.split("::").last.underscore}} => {{subclass}},
{% end %}
})
end
end
JSON::Serializable.use_json_discriminator
has a few problems:The discriminator is considered only during deserialization but not serialization; correctly doing the latter requires defining a read-only instance variable for the discriminator, even though the dynamic type of an object almost always behaves the same as a discriminator. (That exception is when multiple discriminator values map to the same type. I don't think anyone ever does that.) Consider:
The discriminator API should disallow this kind of code. Omitting
@type
altogether also produces an incomplete JSON:So one must write
@type : String
orgetter type : String
, despite this instance variable having no uses outside (de)serialization.use_json_discriminator
is designed for class hierarchies rather than module hierarchies. This won't work due to an infinite recursion inShape.new(JSON::PullParser)
:Likewise, disjoint unions cannot use discriminators, because each variant type must then use
use_json_discriminator "...", {...: self}
and doing so produces a similar infinite recursion.To address these problems, we first consider the case when there is exactly one concrete type; the discriminator then reduces to a single constant field. This field shall be deserialized and serialized even when a corresponding instance variable does not exist. This is still useful in situations where a JSON format might define a magic constant and require all documents to include that constant. A sensible place to declare the constant field is via the
JSON::Serializable::Options
annotation:constants
must be aHashLiteral
or aNamedTupleLiteral
, mapping field names to their expected constant values. This implies every type may use multiple discriminator fields, should the need arise. These literals are chosen because they look like JSON objects.With this, disjoint unions now work out of the box, no extra converters required:
Next, we extend this behaviour to all abstract classes as well: the result of
T.from_json(json)
is simply the first concrete, possibly indirect, subclassU
ofT
such thatU.from_json(json)
returns successfully. This is probably the only sensible interpretation forT.from_json
with an abstractT
. This could be done in e.g.new_from_json_pull_parser
, which currently fails to compile becauseT.allocate
does not exist; the only way to get the new behaviour is to remove any uses ofuse_json_discriminator
in the abstract superclassT
. Then we arrive at the following reimplementation ofuse_json_discriminator
's example code:Likewise, to support modules we check on all includers of
T
, possibly indirect ones, that are classes. Unfortunately, this isn't as straightforward because onlyTypeNode#all_subclasses
exists, but not#all_includers
.JSON::Serializable
'sincluded
hook would also have to be slightly adjusted to support module includers.In the above snippets,
Shape
never needs to be aware that its subtypes use a discriminator field; the types that need discriminators define them right at their own declarations. Indeed, some hierarchies do not require a discriminator at all, because the union interpretation is sufficient. The following shall be supported too:Going in the opposite direction, this treatment now supports discriminators over multiple field names, which are sometimes seen in the wild: (we assume the discriminators are mutually exclusive, or we could include
JSON::Serializable::Strict
)A proof-of-concept implementation is available here. Float and null constants are supported on top of what we have now, but Crystal constants are not ready, nor is YAML serialization. Also it might be worth pre-fetching the constant fields in
new_from_json_pull_parser
/new_from_yaml_node
to speed up the dispatch to subtypes, instead of reading the same constants over and over again in every subtype. Note that the compiler and the standard library themselves don't use discriminators.Related: #8473