python-attrs / cattrs

Composable custom class converters for attrs, dataclasses and friends.
https://catt.rs
MIT License
779 stars 108 forks source link

Register structure hook only for optional types #529

Closed ljnsn closed 3 months ago

ljnsn commented 3 months ago

Description

I'd like to register a structure hook only for optional types. I've search both existing issues and the docs, but please let me know if I missed this somehow.

What I Did

Suppose I receive some data where values can be "" and I want to convert them to None. I only want to convert the fields that are annotated as optional on my model though, fields that are not annotated as optional should raise a validation error.

from typing import Any, Optional

import attrs
import cattrs

converter = cattrs.Converter()

@attrs.define()
class Foo:
    bar: int | None
    baz: int

def _int_or_none(value: Any, _type: type[Any]) -> int | None:
    return None if value == "" else int(value)

I've tried both these, but it seems like they aren't registered at all:

converter.register_structure_hook(Optional[int], _int_or_none)
converter.register_structure_hook(int | None, _int_or_none)

d = {"bar": "", "baz": "2"}
converter.structure(d, Foo)  # with either of the above, tries to convert "" to int

This works, but it also converts non-optional fields to None:

converter.register_structure_hook(int, _int_or_none)
d = {"bar": "", "baz": ""}
converter.structure(d, Foo)  # Foo(bar=None, baz=None)
Tinche commented 3 months ago

Bleh, you're right. It's a quirk of the order in which the default hooks are installed. It's fixable, and I will do so.

In the meantime, you can work around it like this:

from typing import Any

import attrs

import cattrs

converter = cattrs.Converter()

@attrs.define()
class Foo:
    bar: int | None
    baz: int

def _int_or_none(value: Any, _: type[Any]) -> int | None:
    return None if value == "" else int(value)

converter.register_structure_hook_func(lambda t: t == int | None, _int_or_none)

d = {"bar": "", "baz": "2"}
print(converter.structure("", int | None))
ljnsn commented 3 months ago

Great, thanks for the workaround!