Cattrs serializer is not round-trip safe for type B | X (B is a built-in type) when value is not type B
cattrs version: 24.1.3
Hi cattrs team, The round-trip serialization of a union type in the following case is not guaranteed. It is clearly stated in the manual that built-in types are passed through during the serialization. However, the structuring with the same type hint fails. It seems to contradict the intuition that round-trip serialization of the same type should be invertible.
I am aware of the workaround of customizing either the restructuring or structuring for the built-in types. But it also seems to me that these can be handled internally by cattrs.
Thanks a lot.
@attrs.define
class B:
b: int
c = make_converter()
c.structure(c.unstructure(1, float| B), float | B)
# where float can be substituted by any built-in types.
Error:
+-+---------------- 1 ----------------
| Traceback (most recent call last):
| File "<cattrs generated structure __main__.B>", line 5, in structure_B
| res['b'] = __c_structure_b(o['b'])
| TypeError: 'int' object is not subscriptable
| Structuring class B @ attribute b
+------------------------------------
Hello, which converter are you using exactly?
@Tinche Hi Tinche, thanks for looking into this. I used the orjson converter out of the box.
This is actually an interesting case.
You're using the value 1, which is of class int. The target type is float | B.
As far as type checkers are concerned, int is a subclass of float - see https://typing.python.org/en/latest/spec/special-types.html#special-cases-for-float-and-complex. So the type system is fine with this.
At runtime though,
>>> isinstance(1, float)
False
So at runtime, 1 is not an instance of float, hence cattrs failing to validate it.
If you want a quick fix, you can change your type to float | int | B and it will work.
We could probably paper over this Python wart directly in the strategy itself, but it would require special treatment of exactly this case. It probably makes sense to do though, although I can't commit to a timeline (apart from next version).
Fixed it by special-casing the int/float behavior in the next version!
Awesome! Thanks a lot for the nice fix @Tinche.