1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
|
# Conversions – (de)serialization customization
*apischema* covers the majority of standard data types, but of course that's not enough, which is why it enables you to add support for all your classes and the libraries you use.
Actually, *apischema* itself uses this conversion feature to provide a basic support for standard library data types like UUID/datetime/etc. (see [std_types.py](https://github.com/wyfo/apischema/blob/master/apischema/std_types.py))
ORM support can easily be achieved with this feature (see [SQLAlchemy example](examples/sqlalchemy_support.md)).
In fact, you can even add support for competitor libraries like *Pydantic* (see [*Pydantic* compatibility example](examples/pydantic_support.md))
## Principle - apischema conversions
An *apischema* conversion is composed of a source type, let's call it `Source`, a target type `Target` and a converter function with signature `(Source) -> Target`.
When a class (actually, a non-builtin class, so not `int`/`list`/etc.) is deserialized, *apischema* will check if there is a conversion where this type is the target. If found, the source type of conversion will be deserialized, then the converter will be applied to get an object of the expected type. Serialization works the same way but inverted: look for a conversion with type as source, apply then converter, and get the target type.
Conversions are also handled in schema generation: for a deserialization schema, source schema is merged to target schema, while target schema is merged to source schema for a serialization schema.
## Register a conversion
Conversion is registered using `apischema.deserializer`/`apischema.serializer` for deserialization/serialization respectively.
When used as function decorator, the `Source`/`Target` types are directly extracted from the conversion function signature.
`serializer` can be called on methods/properties, in which case `Source` type is inferred to be the owning type.
```python
{!conversions.py!}
```
!!! warning
(De)serializer methods cannot be used with `typing.NamedTuple`; in fact, *apischema* uses the `__set_name__` magic method but it is not called on `NamedTuple` subclass fields.
### Multiple deserializers
Sometimes, you want to have several possibilities to deserialize a type. If it's possible to register a deserializer with a `Union` param, it's not very practical. That's why *apischema* make it possible to register several deserializers for the same type. They will be handled with a `Union` source type (ordered by deserializers registration), with the right serializer selected according to the matching alternative.
```python
{!multiple_deserializers.py!}
```
On the other hand, serializer registration overwrites the previous registration if any.
`apischema.conversions.reset_deserializers`/`apischema.conversions.reset_serializers` can be used to reset (de)serializers (even those of the standard types embedded in *apischema*)
### Inheritance
All serializers are naturally inherited. In fact, with a conversion function `(Source) -> Target`, you can always pass a subtype of `Source` and get a `Target` in return.
Moreover, when serializer is a method/property, overriding this method/property in a subclass will override the inherited serializer.
```python
{!serializer_inheritance.py!}
```
!!! note
Inheritance can also be toggled off in specific cases, like in the [Class as union of its subclasses](examples/subclass_union.md) example
On the other hand, deserializers cannot be inherited, because the same `Source` passed to a conversion function `(Source) -> Target` will always give the same `Target` (not ensured to be the desired subtype).
!!! note
Pseudo-inheritance could be achieved by registering a conversion (using for example a `classmethod`) for each subclass in `__init_subclass__` method (or a metaclass), or by using `__subclasses__`; see [example](examples/inherited_deserializer.md)
## Generic conversions
`Generic` conversions are supported out of the box.
```python
{!generic_conversions.py!}
```
However, you're not allowed to register a conversion of a specialized generic type, like `Foo[int]`.
## Conversion object
In the previous example, conversions were registered using only converter functions. However, it can also be done by passing a `apischema.conversions.Conversion` instance. It allows specifying additional metadata to conversion (see [next sections](#sub-conversions) for examples) and precise converter source/target when annotations are not available.
```python
{!conversion_object.py!}
```
## Dynamic conversions — select conversions at runtime
Whether or not a conversion is registered for a given type, conversions can also be provided at runtime, using the `conversion` parameter of `deserialize`/`serialize`/`deserialization_schema`/`serialization_schema`.
```python
{!dynamic_conversions.py!}
```
!!! note
For `definitions_schema`, conversions can be added with types by using a tuple instead, for example `definitions_schema(serializations=[(list[Foo], foo_to_bar)])`.
The `conversion` parameter can also take a tuple of conversions, when you have a `Union`, a `tuple` or when you want to have several deserializations for the same type.
### Dynamic conversions are local
Dynamic conversions are discarded after having been applied (or after class without conversion having been encountered). For example, you can't apply directly a dynamic conversion to a dataclass field when calling `serialize` on an instance of this dataclass. Reasons for this design are detailed in the [FAQ](#whats-the-difference-between-conversion-and-default_conversion-parameters).
```python
{!local_conversions.py!}
```
!!! note
Dynamic conversion is not discarded when the encountered type is a container (`list`, `dict`, `Collection`, etc. or `Union`) or a registered conversion from/to a container; the dynamic conversion can then apply to the container elements
### Dynamic conversions interact with `type_name`
Dynamic conversions are applied before looking for a ref registered with `type_name`
```python
{!dynamic_type_name.py!}
```
### Bypass registered conversion
Using `apischema.identity` as a dynamic conversion allows you to bypass a registered conversion, i.e. to (de)serialize the given type as it would be without conversion registered.
```python
{!bypass_conversions.py!}
```
!!! note
For a more precise selection of bypassed conversion, for `tuple` or `Union` member for example, it's possible to pass the concerned class as the source *and* the target of conversion *with* `identity` converter, as shown in the example.
### Liskov substitution principle
LSP is taken into account when applying dynamic conversion: the serializer source can be a subclass of the actual class and the deserializer target can be a superclass of the actual class.
```python
{!dynamic_conversions_lsp.py!}
```
### Generic dynamic conversions
`Generic` dynamic conversions are supported out of the box. Also, contrary to registered conversions, partially specialized generics are allowed.
```python
{!dynamic_generic_conversions.py!}
```
## Field conversions
It is possible to register a conversion for a particular dataclass field using `conversion` metadata.
```python
{!field_conversions.py!}
```
!!! note
It's possible to pass a conversion only for deserialization or only for serialization
## Serialized method conversions
Serialized methods can also have dedicated conversions for their return
```python
{!serialized_conversions.py!}
```
## Default conversions
As with almost every default behavior in *apischema*, default conversions can be configured using `apischema.settings.deserialization.default_conversion`/`apischema.settings.serialization.default_conversion`. The initial value of these settings are the function which retrieved conversions registered with `deserializer`/`serializer`.
You can for example [support *attrs*](examples/attrs_support.md) classes with this feature:
```python
{!examples/attrs_support.py!}
```
*apischema* functions (`deserialize`/`serialize`/`deserialization_schema`/`serialization_schema`/`definitions_schema`) also have a `default_conversion` parameter to dynamically modify default conversions. See [FAQ](#whats-the-difference-between-conversion-and-default_conversion-parameters) for the difference between `conversion` and `default_conversion` parameters.
## Sub-conversions
Sub-conversions are [dynamic conversions](#dynamic-conversions--select-conversions-at-runtime) applied on the result of a conversion.
```python
{!sub_conversions.py!}
```
Sub-conversions can also be used to [bypass registered conversions](#bypass-registered-conversion) or to define [recursive conversions](#lazyrecursive-conversions).
## Lazy/recursive conversions
Conversions can be defined lazily, i.e. using a function returning `Conversion` (single, or a tuple of it); this function must be wrapped into a `apischema.conversions.LazyConversion` instance.
It allows creating recursive conversions or using a conversion object which can be modified after its definition (for example a conversion for a base class modified by `__init_subclass__`)
It is used by *apischema* itself for the generated JSON schema. It is indeed a recursive data, and the [different versions](json_schema.md#json-schema--openapi-version) are handled by a conversion with a lazy recursive sub-conversion.
```python
{!recursive_conversions.py!}
```
### Lazy registered conversions
Lazy conversions can also be registered, but the deserialization target/serialization source has to be passed too.
```python
{!lazy_registered_conversion.py!}
```
## Conversion helpers
### String conversions
A common pattern of conversion concerns classes that have a string constructor and a `__str__` method, for example standard types `uuid.UUID`, `pathlib.Path`, or `ipaddress.IPv4Address`. Using `apischema.conversions.as_str` will register a string-deserializer from the constructor and a string-serializer from the `__str__` method. `ValueError` raised by the constructor is caught and converted to `ValidationError`.
```python
{!as_str.py!}
```
!!! note
Previously mentioned standard types are handled by *apischema* using `as_str`.
### ValueErrorCatching
Converters can be wrapped with `apischema.conversions.catch_value_error` in order to catch `ValueError` and reraise it as a `ValidationError`. It's notably used but `as_str` and other standard types.
!!! note
This wrapper is in fact inlined in deserialization, so it has better performance than writing the *try-catch* in the code.
### Use `Enum` names
`Enum` subclasses are (de)serialized using values. However, you may want to use enumeration names instead, that's why *apischema* provides `apischema.conversion.as_names` to decorate `Enum` subclasses.
```python
{!as_names.py!}
```
### Class as union of its subclasses
### Object deserialization — transform function into a dataclass deserializer
`apischema.objects.object_deserialization` can convert a function into a new function taking a unique parameter, a dataclass whose fields are mapped from the original function parameters.
It can be used for example to build a deserialization conversion from an alternative constructor.
```python
{!object_deserialization.py!}
```
!!! note
Parameters metadata can be specified using `typing.Annotated`, or be passed with `parameters_metadata` parameter, which is a mapping of parameter names as key and mapped metadata as value.
### Object serialization — select only a subset of fields
`apischema.objects.object_serialization` can be used to serialize only a subset of an object fields and methods.
```python
{!object_serialization.py!}
```
## FAQ
#### What's the difference between `conversion` and `default_conversion` parameters?
Dynamic conversions (`conversion` parameter) exists to ensure consistency and reuse of subschemas referenced (with a `$ref`) in the JSON/OpenAPI schema.
In fact, different global conversions (`default_conversion` parameter) could lead to having a field with different schemas depending on global conversions, so a class would not be able to be referenced consistently. Because dynamic conversions are local, they cannot mess with an object field schema.
Schema generation uses the same default conversions for all definitions (which can have associated dynamic conversion).
`default_conversion` parameter allows having different (de)serialization contexts, for example to map date to string between frontend and backend, and to timestamp between backend services.
|