I’m playing around with implementing ActivityPub where most of the entities over the JSON API inherit from other types. For example, Create < Activity < Object
. And that works as long as I don’t define an initialize
method that lets me instantiate them myself to be able to send the instances to other servers. This snippet illustrates what I’m trying to do:
require "json"
abstract class A
include JSON::Serializable
getter a1 : String?
getter a2 : String?
def initialize(*, @a1 = nil, @a2 = nil)
end
end
class B < A
getter b1 : String? = nil
def initialize(*, @b1 = nil, **kwargs)
super(**kwargs)
end
end
pp B.new(
a1: "a1",
b1: "b1",
)
pp B.from_json({
a1: "a1",
b1: "b1",
}.to_json)
This represents some superclass A
that has a bunch of properties common to all ActivityPub objects, and then subclasses add extra ones. But it doesn’t compile:
Showing last frame. Use --error-trace for full trace.
In examples/stuff.cr:17:5
17 | super(**kwargs)
^----
Error: no parameter named '__pull_for_json_serializable'
Matches are:
- A#initialize(*, a1 = nil, a2 = nil)
shell returned 1
Are there some semantics I’m overlooking? Can anyone think of any workarounds?
Just remove the *
from your initializers and it’ll work. Alternatively, duplicate the params on each constructor/super call to avoid Crystal thinking you’re calling the JSON::Serializable
overload.
Unfortunately, this does not work. I still got the same error from this code:
require "json"
abstract class A
include JSON::Serializable
getter a1 : String?
getter a2 : String?
def initialize(@a1 = nil, @a2 = nil)
end
end
class B < A
getter b1 : String? = nil
def initialize(@b1 = nil, **kwargs)
super(**kwargs)
end
end
pp B.new(
a1: "a1",
b1: "b1",
)
pp B.from_json({
a1: "a1",
b1: "b1",
}.to_json)
This does work, however, which is a bit surprising. I feel like since kwargs
is known at compile time to be a NamedTuple
with known keys, double-splatting it would know to call the @a1 = nil, @a2 = nil
overload.
Something I just thought about while writing the above paragraph, though, is that in that overload, every argument is optional, and I wondered if that made a difference. So I experimented with removing some of the default nil
values. It compiles as long as @b1
is not an optional argument. Does this sound like a bug or is it unreasonable to expect the type checker to figure this out?
Hmm, are you sure? Carcin Seems to work fine no?
Oooh, I see, it’s not super obvious from the code snippet in the post but there’s more code if you scroll. Once you add the .from_json
call, it stops compiling.
Ahh okay, yea that does it. I think what’s ultimately happening is you’re redefining the initializer defined by JSON::Serializable
, which .from_json
ends up calling. Tho I’m not sure if this is expected or a bug given Crystal in other cases is able to figure it out: Carcin. So reducing the code to figure out what the trigger is might be helpful.
EDIT: By reduce I mean try and figure out what exact combination of constructors/inheritance causes it, or if its strictly related to the included module/inherited hook.
Ahh, that sounds right. I seem to remember something built into JSON::Serializable
to handle that scenario (a redefinition of the initialize
method inside an inherited
macro, I think?), but looking through the file history I don’t see it, so I must’ve imagined it.
Wait, the thing I was apparently imagining actually works, though
Only difference here is the redefinition of initialize
for the JSON::PullParser
:
require "json"
abstract class A
include JSON::Serializable
getter a1 : String?
getter a2 : String?
def initialize(*, __pull_for_json_serializable pull : ::JSON::PullParser)
super
end
macro inherited
def initialize(*, __pull_for_json_serializable pull : ::JSON::PullParser)
super
end
end
def initialize(*, @a1 = nil, @a2 = nil)
end
end
class B < A
getter b1 : String? = nil
def initialize(*, @b1 = nil, **kwargs)
super(**kwargs)
end
end
pp B.new(
a1: "a1",
b1: "b1",
)
pp B.from_json({
a1: "a1",
b1: "b1",
}.to_json)
carcin
FYI, I commented out the macro inherited .. end
part from that Carcin, and it works:
https://play.crystal-lang.org/#/r/d56l
1 Like
Nice! This class hierarchy has a few layers so that might be something that needs to stay, but I’m not sure.