schematics/schematics — pull request #269 by bintoro (2014-12-21 00:52:29+00:00)
Title: Marshallers and extended serializables for better data manipulation at import/export time

The `serializable` decorator currently provides a simple way for a model to expose a virtual field. However, the mechanism doesn't handle two-way communication.

Consider a model with fields `first_name` and `last_name` that needs to talk to a client that only sends and receives a single value, `full_name`:

```python
class Person(Model):

first_name = StringType()
last_name = StringType()

@serializable
def full_name(self):
return '%s %s' % (self.first_name, self.last_name)
```

The above works for outbound data, but what about the reverse? How to split the incoming value into two concrete fields?

This PR introduces two facilities that provide pre- and post-processing hooks for data manipulation.

### Extended serializables

Instead of decorating a function, decorate a class that encloses functions for both exporting and importing:

```python
@serializable
class full_name:

def serialize(self):
return '%s %s' % (self.first_name, self.last_name)

def deserialize(value):
first_name, last_name = value.split()
return {
'first_name': first_name,
'last_name': last_name
}
```

The wrapper class (`full_name`) is nothing but a closure. It will never be instantiated, and inside the `serialize` function `self` still refers to the `Person` instance. In other words, `serialize()` here is identical to the classic one-way serializable.

The dictionary returned by `deserialize()` will be added to the dataset, and the original incoming field (`full_name`) will be discarded.

### Marshalling/unmarshalling functions

These are functions that can perform arbitrary operations on the entire dataset.

```python
class Person(Model):

first_name = StringType()
last_name = StringType()

def marshal_full_name(cls, data):
data['full_name'] = '%s %s' % (data['first_name'], data['last_name'])

def unmarshal_full_name(cls, data):
if 'full_name' not in data:
raise ModelConversionError("Field 'full_name' missing")
first_name, last_name = data['full_name'].split()
new_data = {
'first_name': first_name,
'last_name': last_name
}
return (new_data, ('full_name',))
```

An **unmarshaller** (processes inbound data) must return a tuple with two items:

* a dictionary that will be added to the final dataset
* an iterable of keys that should be discarded.

A **marshaller** (processes outbound data) may do the same, or it can modify the dataset directly and return nothing, as shown above.

Apart from the `(un)marshal_` prefix, the names of the functions are of no consequence (except for error info), since they do not necessarily relate to just a single field. A model-wide pre/post-processor can be named simply `unmarshal` or `marshal`.

### Comparison

* Serializables deal with native values: they are invoked after `to_native()` upon import and before `to_primitive()` upon export.

* Marshalling functions deal with primitive values: they are called before `to_native()` upon import and after `to_primitive()` upon export.

* Serializables always map to **one** exposed field. If you wanted to do the opposite of the example and map multiple incoming values to a single concrete field, a serializable will not do.

* Marshalling is more powerful, since the entire dataset can be freely manipulated.

* The unmarshallers are run before anything else is done to the incoming data. For example, multiple values may be coalesced into one, and the surplus transport fields can be dropped before rogue fields are checked for.

* A serializable is always available as `instance.some_serializable`, whereas marshallers are only evaluated at export time.

### Also changed

Serializables now accept the parameters `default`, `required`, and `deserialize_from`. Note that `required` on a serializable is only enforced on import — not during validation.

The default value for `serialize_when_none` on serializables now matches that on types (changed from `True` to `None`).