User guide

Create models

You can use models library for two different things:

  1. Create data representation and validate incoming data quickly.

  2. Display data schema in UI (forms, schemas, etc.).

To create a custom model just inherit from the base Model class. Recommended way to do it, which would preserve python typing hints, is shown below.

>>> from kaiju_models import *
>>> class User(Model["User.Fields"]):
...     class Fields(Model.Fields):
...         name: str = StringField(min_length=3, required=True)
...         registered: bool = BooleanField(default=False)
...         nicknames: list[str] = ListField(StringField())

Since model type is a subclass of a field type, you can use models in fields as well. Adding a model as a field will result to this field being a msgspec struct. It’s recommended to type hint such inner models with their Fields attribute. Fields that accept other fields as arguments (for example ListField) also can accept models.

Here is a more complex example of a model above which uses these features.

>>> class Identifier(Model["Phone.Fields"]):
...     class Fields(Model.Fields):
...         id: str = StringField(required=True)
...         type: str = EnumField(["phone", "email"], required=True)
...         verified: bool = BooleanField(default=False)
>>> class Address(Model["Address.Fields"]):
...     class Fields(Model.Fields):
...         city: str = StringField(required=True)
...         street: str = StringField()
>>> class User(Model["UserModel.Fields"]):
...     class Fields(Model.Fields):
...         name: str = StringField(min_length=3, required=True)
...         registered: bool = BooleanField(default=False)
...         nicknames: list[str] = ListField(StringField())
...         identifier: Identifier.Fields = Identifier(required=True)
...         address: list[Address.Fields] = ListField(Address())

Validate data

Once the class is initialized, a msgspec.struct object will be generated based on the model fields description. You can either access it by __base_type__ attribute, or you can directly validate or load data using the class methods get_struct(), decode(), decode_msgpack().

The latter two are basically shortcuts for converting binary data from HTTP requests and other sources in the most efficient way. It’s recommended to use it when converting from JSON since it retains type information and therefore faster. The resulting object is always a msgspec.struct object.

>>> user_model = UserModel()
>>> user = user_model.decode(b'{"name":"John","registered":true,"identifier":{"type":"phone","id":"888769"}}')
>>> assert user.name == 'John'

get_struct() method can be used both with dictionary like objects or normal objects with attributes, as long as it matches the expected structure interface.

>>> data = {"name": "John", "registered": True, "identifier": {"type": "phone", "id": "888769"}}
>>> user = user_model.decode(data)
>>> assert user.name == 'John'
>>> from collections import namedtuple
>>> data = namedtuple('Userdata', ('name', 'registered', 'identifier'), ('John', True, {'type': 'phone', 'id': '888769'}))
>>> user = user_model.decode(data)
>>> assert user.name == 'John'

If the data is invalid then a conversion method raises ModelValidationError with a bunch of additional data about error type and constraints. Note that it only checks for one value at a time.

To dump data once again it’s efficient to use the model encoder since it is already pre-initialized for this data type. Use encode() or encode_msgpack() to convert a struct to binary data.

>>> data = user_model.encode(user)

Msgpack

Msgpack encode_msgpack() and decode_msgpack() are differ slightly from normal encode methods. Aside from using messagepack data format they also use compact data representation. Each struct is encoded as an array with all the default values skipped. It also uses certain optimization in encoding particular data types. Keep that in mind whenever you want to manually decode and parse msgpack model output.

Msgpack encoding can safe up to 50% space / traffic and is also pretty CPU efficient.

Integrate with UI

There is a specific method get_schema() which returns a model schema in UI friendly format. One can use it to dynamically create a form for model data. Only fields and models marked with ui_visible=True are returned in the list of fields. The output of the method would look like this in JSON:

{
  "type": "model",
  "title": null,
  "schema": [
    {
      "id": "name",
      "type": "str",
      "default": null,
      "title": null,
      "description": null,
      "meta": {
        "form_properties": {
          "has_feedback": false,
          "placeholder": null,
          "disable_label": false,
          "mark_required": false
        }
      },
      "validation": {
        "required": null,
        "min_length": 3,
        "max_length": null,
        "pattern": null,
        "pattern_error_text": null
      }
    },
    ...
  ],
  "meta": {
    "form_properties": {
      "has_feedback": false,
      "placeholder": null,
      "disable_label": false,
      "mark_required": false
    }
  },
  "ordering": [
    "registered",
    "nicknames",
    "identifier",
    "has_money",
    "address",
    "sex",
    "birthdate",
    "bio",
    "name"
  ]
}

Each model field or submodel has its own sub-schema. The ordering field at each model is a special containing an ordered list of attributes is a special type of field which can only present in models and tells the API in which order the fields should be if using compact array data format.

The proper way to use models API with UI is to create a UI validation form to pre-validate data on the client. The model validation methods should be reserved for data conversion and API calls, so if data doesn’t pass validation on the backend it may be a signal that there’s something wrong going on. However it’s still a good idea to process backend validation errors on the client side at least to some extent. See ErrorData for the description of data provided by ModelValidationError class.

Dynamic models

Model classes can be created dynamically from fields using create_model_type(). The function returns a new model class and also registers it by its name in TYPES registry of field and model classes.

>>> new_user_model = create_model_type('NewUserModel', user_model.Fields.__fields__)

Model sharing

For dynamic models (or sometimes even for static ones) it may prove useful to share them across instances, or store them at a file / database. You can use repr_() and from_repr() for that purpose, or rather encode_model() and decode_model() if you need to convert models from / to binary data.

The process itself is straightforward.

>>> data = encode_model(user_model)
>>> user_model = decode_model(data)

Note that obviously it needs to have all the respectful model and field types defined in the system to be able to load. By default serialization functions use TYPES global store for all model and field types where classes automatically added on class creation using __init_subclass__ hook.

You can pass different types mapping to the function if needed.