Remark: If you are in a situation where you want to serialize a trait object, please take a step back.
Check if you can replace your trait object with an enum.
In my experience, the enum approach is much easier to work with.
Remark: All topics covered here are well-known. We follow typetag.
So, let’s start.
Today, our quest is to deserialize a trait object.
This will be an exercise in the visitor pattern.
We start with the following code, which defines a trait and a struct implementing it.
We try to deserialize a trait object instance.
Our json input will be:
which is a “key-value map” {key: value} in the serde world, and corresponds to the externally tagged serialization of an enum1.
In our example, {“S”: {“data”:0}}, we have the key/type-info “S” and the value/type-serialization {“data”: 0}.
To implement Deserialize, we start with the following snippet.
Note that the “output” will be a boxed trait object, which is “just a type” (as opposed to a trait, and it is “owned”2).
Since we want to deserialize a “key-value map”, we want to call deserializer.deserialize_map, so we need a visitor:
In our visitor helper, we first need to deserialize the key.
Recall that we serialized the key as a String.
At this point, I always prefer to see the code in action, so here is a debuggable snippet, which compiles, outputs the deserialized type information, and panics.
Let’s take a step back. What do we know now?
We want to deserialize some json into a boxed trait object, Box<dyn Trait>.
After serialization of the type information, we know that our underlying type is S.
The type S implements Deserialize
So we deserialize the remaining part of our json into an instance of our type S.
Finally, we cast our instance of S into a boxed trait object
Let’s implement this:
This works and compiles, so we are done.
We are done, right?
Not?
Why not?
Well, we knew from our type_info that the underlying type is S, but given some other json, we might find some other type.
As of now, we hard-coded that the serialized type has to be S. In the enum correspondence, we assume that our enum has a single variant.
Let’s try to be a bit for flexible. To this end we use map.next_value_seed instead of map.next_value. Visitors for the rescue!
Recall from our dotnet-digression in Part 1, that we need a runtime-reflection mechanism at some point.
This mechanism will be covered in the next part.
Today, I want to finish the deserialization machine.
First, we introduce an abstract deserialization function.
We need this to be non-generic, so we employ erased_serde as follows:
Next, we enhance our visitor:
Finally, we generate a dummy runtime-reflection:
This works!
Now, given our runtime_reflection function, we can deserialize any instance of our trait object. Hurray!
Here is a complete code snippet:
For fun, let’s rewrite the closure in our runtime-reflection.
Currently, we have:
And here is some nice, generic code.
The bounds are actually requested by the compiler, so we just add them.
3
Using this, we update our runtime-reflection function, which looks already quite good.
That’s it for today.
Thank you for following the blog series.
Next time we will proceed with runtime reflection and replace the hard-coded
by a lookup-table.
Footnotes
Personally, I prefer the internally tagged variant. But this is quite more involved to code. There might be a later post about it. If you are interested in reading about it, please give me some feedback. ↩
I’m referring to the fact that Box<dyn Trait> actually means Box<dyn Trait + 'static>. Part 5 is about lifetimes. ↩
Recall from the other footnote that Box<dyn Trait> actually means Box<dyn Trait + 'static>. Hence, the bound A:'static is not surprising. More on lifetimes is discussed in part 5. ↩