Structures

structures are used to parse complex objects and return a dictionary.

Definitions

Its definition is as follows:

dog = z.struct({
    'name': z.field(z.str()),
    'breed': z.field(z.str()),
})

If you do not need to make any changes to the field, you can omit field. The above definition can be simplified to the following code:

dog = z.struct({
    'name': z.str(),
    'breed': z.str(),
})

The structure field, when parsing an object, will use its field name to get the value of the key for Mapping objects, and the value of the attribute for other objects.

# Parsing a Mapping object

>>> dog.parse({
...     'name': 'Fido',
...     'breed': 'bulldog'
... })
{'name': 'Fido', 'breed': 'bulldog'}

# Parsing other objects

>>> from types import SimpleNamespace

>>> obj = SimpleNamespace(name='Fido', breed='bulldog')
>>> dog.parse(obj)
{'name': 'Fido', 'breed': 'bulldog'}

Optional Fields

Fields are required by default, but this method allows them to be made optional.

>>> dog = z.struct({
...     'name': z.str(),
...     'breed': z.field(z.str()).optional(),
... })

>>> dog.parse({'name': 'Fido'})
{'name': 'Fido'}

It is also possible to provide a default value for the optional field.

>>> dog = z.struct({
...     'name': z.str(),
...     'breed': z.field(z.str()).optional(default='unknown'),
... })

>>> dog.parse({'name': 'Fido'})
{'name': 'Fido', 'breed': 'unknown'}

Field Alias

You can assign alias to external data corresponding to field, which will be mapped to field name during parsing.

>>> dog = z.struct({
...   'name': z.field(z.str(), alias='nickname'),
... })

>>> dog.parse({'nickname': 'Fido'})
{'name': 'Fido'}

Extending Fields

You can expand the field in the following way.

>>> dog_with_age = z.struct({
...   **dog.fields,
...   'age': z.int(),
... })

.ensure_fields

🚧 TODO: Missing description…

my_schema = z.struct({
    'start_time': z.field(z.datetime()),
    'end_time': z.field(z.datetime()),
})
>>> from datetime import datetime

>>> my_schema.ensure(lambda data: data['end_time'] > data['start_time'], message='The end time cannot be later than the start time').parse({
...     'start_time': datetime(2000, 1, 2),
...     'end_time': datetime(2000, 1, 1),
... })
Traceback (most recent call last):
zangar.exceptions.ValidationError: [{'msgs': ['The end time cannot be later than the start time']}]

>>> my_schema.ensure_fields(['end_time'], lambda data: data['end_time'] > data['start_time'], message='The end time cannot be later than the start time').parse({
...     'start_time': datetime(2000, 1, 2),
...     'end_time': datetime(2000, 1, 1),
... })
Traceback (most recent call last):
zangar.exceptions.ValidationError: [{'msgs': ['The end time cannot be later than the start time'], 'loc': ['end_time']}]

Change Optional Fields

Two opposite methods, optional_fields and required_fields, are provided to change optional fields.

.optional_fields

Based on the current structure schema fields, construct a new structure schema, setting all fields as optional; or set specified fields as optional and the other fields as required. The opposite of .required_fields.

z.struct(z.optional_fields({
    'username': z.field(z.str()),
    'email': z.field(z.str())
}))

# equivalent to:
z.struct({
    'username': z.field(z.str()).optional(),
    'email': z.field(z.str()).optional()
})
z.struct(z.optional_fields({
    'username': z.field(z.str()),
    'email': z.field(z.str())
}, ['username']))

# equivalent to:
z.struct({
    'username': z.field(z.str()).optional(),
    'email': z.str()
})

.required_fields

Based on the current structure schema fields, construct a new structure schema, setting all fields as required; or set specified fields as required and the other fields as optional. The opposite of .optional_fields.

z.struct(z.required_fields({
    'username': z.field(z.str()).optional(),
    'email': z.field(z.str()).optional()
}))

# equivalent to:
z.struct({
    'username': z.str(),
    'email': z.str()
})
z.struct(z.required_fields({
    'username': z.field(z.str()).optional(),
    'email': z.field(z.str()).optional()
}, ['username']))

# equivalent to:
z.struct({
    'username': z.str(),
    'email': z.field(z.str()).optional()
})

Pick or Omit Fields

.pick_fields

Construct a new structure schema by selecting the specified fields. The opposite of .omit_fields.

z.struct(z.pick_fields({
    'username': z.str(),
    'email': z.str()
}, ['username']))

# equivalent to:
z.struct({
    'username': z.str()
})

.omit_fields

Construct a new structure schema by excluding the specified fields. The opposite of .pick_fields.

z.struct(z.omit_fields({
    'username': z.str(),
    'email': z.str()
}, ['username']))

# equivalent to:
z.struct({
    'email': z.str()
})

Unknown Fields

Only mstruct can access unknown fields.

Include unknown fields in the parsed result.

>>> z.mstruct({
...     'username': z.str(),
...     'email': z.str()
... }, unknown='include').parse({
...     'username': 'john',
...     'email': '[email protected]',
...     'age': 18
... })
{'username': 'john', 'email': '[email protected]', 'age': 18}

Raise an error when encountering unknown fields.

>>> z.mstruct({
...     'username': z.str(),
...     'email': z.str()
... }, unknown='raise').parse({
...     'username': 'john',
...     'email': '[email protected]',
...     'age': 18
... })
Traceback (most recent call last):
zangar.exceptions.ValidationError: [{'msgs': ['Unknown field'], 'loc': ['age']}]

Working with Dataclass

Python’s dataclass does not support data validation by default. However, with Zangar, you can now add validation to it.

You need to add metadata named “zangar” to each dataclass field. The value of this metadata will be used as the argument for zangar.field.

from dataclasses import dataclass, field

import zangar as z

@dataclass
class InventoryItem:
    """Class for keeping track of an item in inventory."""
    name: str = field(metadata={'zangar': {'schema': z.str()}})
    unit_price: float = field(metadata={'zangar': {'schema': z.float()}})
    quantity_on_hand: int = field(default=0, metadata={'zangar': {'schema': z.int()}})
# It's good!
>>> invertory_schema = z.dataclass(InventoryItem)
>>> invertory_schema.parse({'name': "necklace", 'unit_price': 12.50})
InventoryItem(name='necklace', unit_price=12.5, quantity_on_hand=0)

# It's bad! because unit_price is not a float.
>>> z.dataclass(InventoryItem).parse({'name': "necklace", 'unit_price': '12.50'})
Traceback (most recent call last):
zangar.exceptions.ValidationError: [{'msgs': ['Expected float, received str'], 'loc': ['unit_price']}]

Dataclass to Struct

You can create a struct with dataclass fields

>>> z.mstruct(invertory_schema.fields, unknown='include').parse({'name': "necklace", 'unit_price': 12.50, 'from': "China"})
{'name': 'necklace', 'unit_price': 12.5, 'quantity_on_hand': 0, 'from': 'China'}