About
This documentation covers the models system of {typed} which is used specifically to data validation.
Overview
The models system is defined in the typed.models
module, such that:
it provides model factories, which are type factories constructing models;
the difference between the kinds of model factories is how data is validated in instances of their models;
each model factory has a corresponding model type, consisting of all its models;
the process of creating models is compatible with other sources of data validation structures, as Python dataclasses and pydantic
BaseModel
.
Model Factories
Recall that in {typed} we have type factories, which are functions that return a type, hence that take values into the metatype TYPE
of all types. Recall, yet, that we have a primitive type Json
.
In a very general perspective, a model factory is a type factory such that:
it has only keyword arguments;
the value of each
kwarg
is a type;the key of each
kwarg
defines an attribute in the returning type;its returning type is a subtype of
Json
.
In other words, a type factory is a function ModelFactory: Dict(TYPE) -> TYPE
such that:
model = ModelFactory(**kwargs)
is a subtype ofJson
model.key
is defined for eachkey
inkwargs
.
We have the following model factories:
model factory |
description |
---|---|
Model |
the factory of _basic_ modes |
Exact |
the factory of _strict_ models |
Ordered |
the factory of _ordered_ models |
Rigid |
the factory of _rigid_ models |
Validation Conditions
In {typed}, a model is precisely a type which is constructed from a model factory. In other words, it is an instance model = ModelFactory(**kwargs)
for some ModelFactory
listed above, and some kwargs
in Dict(TYPE)
.
The difference between the model factories is precisely how their models are validated.
More precisely, since the model factories produces subtypes of Json
, it follows that if isinstance(x, model)
is True
, then x
is a Json
data. Each key
in kwargs
corresponds to an attribute of model
, which, in turn, corresponds to an entry in each instance x
of model
, when viewed as a Json
datas. The different flavors of model factories deals, precisely, with the reciprocal question:
Take a model factory
ModelFactory
. Then, given an objectx
such thatisinstance(x, Json)
isTrue
, which conditionsx
should satisfy such thatisinstance(x, model)
isTrue
formodel = ModelFactory(**kwargs)
for somekwargs
inDict(TYPE)
?
The answers for each question are in the following table:
model factory |
validation condition |
---|---|
Model |
the json data contains at least the defined attributes |
Exact |
the json data contains precisely the defined attributes |
Ordered |
the json data contains at least the defined attributes, but in the same ordering |
Rigid |
the json data contains precisely the defined attributes, and in the same ordering |
Model Types
Each model factory defines a type whose instances are its models:
model factory |
models type |
---|---|
Model |
MODEL |
Exact |
EXACT |
Ordered |
ORDERED |
Rigid |
RIGID |
So, isinstance(x, MODEL)
is True
iff isinstance(x, TYPE)
is True
, and x = Model(**kwargs)
for some dictionary kwargs
of type Dict(TYPE)
. Analogously for the other model factories and model types.
Defining Models
Let ModelFactory
be a generic model factory, i.e, some of that in table 1. The typical way to define a model for it, is as follows:
1from typed import SomeType, OtherType, ...
2from typed.models import ModelFactory
3
4MyModel = ModelFactory(
5 some_attr: SomeType,
6 other_attr: OtherType,
7 ...
8)
So, for example, we define an instance of the model type MODEL
as below (and Analogously for the other model factories and model types):
1from typed import SomeType, OtherType, ...
2from typed.models import Model
3
4MyModel = Model(
5 some_attr: SomeType,
6 other_attr: OtherType,
7 ...
8)
9
10print(isinstance(MyModel, MODEL)) # returns True
Decorators
There is another way to define models, which is using the model decorators. Indeed, to each model factory there corresponds a namesake decorator, named in lowercase:
model factor |
decorator |
---|---|
Model |
@model |
Exact |
@exact |
Ordered |
@ordered |
Rigid |
@rigid |
The decorators can be applied to any class. This will generate a model of the corresponding model type, based on the attributes of the given class. This can be used to quickly create models, as follows (same for other model types by making use of other model decorators):
1from typed import SomeType, OtherType, ...
2from typed.models import model
3
4@model
5class MyModel:
6 some_attr: SomeType
7 other_attr: OtherType
8 ...
This approach can also be used to convert models from other sources into {typed} models. In particular, this can be used to generate {typed} models from pydantic models and from Python dataclasses.
The approach of creating {typed} models from model decorators is recommended. Indeed, while created directly from a model factory, the model a fully dynamically entity. This means that a LSP will not collect its attributes. On the other hand, while using the model decorators, they are applied to static classes, whose attributes are recognized by LSPs, providing a better developing experience.
Universal Decorator
Actually, one can use @model
as an “universal decorator”, in the sense that one can reconstruct the behavior of the other model decorators from it. Indeed, @model
contains boolean variables, as below, which, when set, introduce the corresponding decorator in the model construction.
variable |
model decorator |
---|---|
exact |
@exact |
ordered |
@ordered |
rigid |
@rigid |
For example, one could create an exact model as follows:
1from typed import SomeType, OtherType, ...
2from typed.models import model
3
4@model(exact=True)
5def MyModel:
6 some_attr: SomeType
7 other_attr: OtherType
8 ...
9
10print(isinstance(MyModel, EXACT)) # prints True
Data Validation
One time defined a model, one can use it to:
validate existing
Json
data;create already validated
Json
data.
Indeed, suppose we have a given json_data
. The validation can be realized by calling the model with the given Json
data:
1
2json_data = {
3 "some_attr": some_value,
4 "other_attr": other_value
5 ...
6}
7
8validated_data = MyModel(**json_data)
Another way of validating a predefined json_data
is through the Validate
function:
1from typed.models import Validate
2
3validated_data = Validate(
4 model=MyModel,
5 data=json_data
6)
The simplest way to create validated data is to call the model with specified kwargs
:
1validated_data = MyModel(
2 some_attr=some_value,
3 other_attr=other_value,
4 ...
5)
Independently of the case, if some condition used in the definition of the model MyModel
is not satisfied, a TypeError
will be raised.
For more on the {typed} error messages, see errors.
Optional Attributes
In the moment of the definition of a model, one can determine certain attributes as optional. If the model is defined from a model factory, this can be done my making use of the Optional: TYPE x Any -> TYPE
directive, which receives an attribute and a default value:
1from typed import SomeType, OtherType, OptType
2from typed.models import ModelFactory, Optional
3
4MyModel = ModelFactory(
5 some_attr=SomeType,
6 other_attr=OtherValue,
7 opt_atrr=Optional(OptType, default_value),
8 ...
9)
Then, in the validation of some json_data
through the model MyModel
, if there is no entry named opt_attr
, the validated data will be appended with such entry with value given by default_value
.
The directive Optional
also accept to do not pass a default value, i.e, it accepts a single argument Optional(OptType)
. In this case:
First it will be checked if passed type
OptType
isnullable
. In this case,Optional(OptType)
will returnOptional(OptType, null(OptType))
, i.e, the null object ofOptType
will be set as the default argument.If
OptType
is notnullable
, it will be checked if it can be initialized. In this case,Optional(OptType)
will returnOptional(OptType, OptType())
.Finally, if not of the above conditions are satisfied, then
None
will be set as the default value. More precisely,Optional(OptType)
will returnOptional(Maybe(OptType), None)
.
In the definition of a model from a model decorator, an optional attribute can be defined by just setting a default value, or by making use of the Optional
directive:
1from typed import SomeType, OtherType, OptType
2from tped.models import model, Optional
3
4@model
5class MyModel:
6 some_attr: SomeType
7 other_attr: OtherType
8 opt_attr: OptType=default_value
9 ...
10
11@model
12class MyModel:
13 some_attr: SomeType
14 other_attr: OtherType
15 opt_attr: Optional(OptType, default_value)
16 ...
The difference, here, is that you can customize the behavior of passing a single argument to Optional
through the variable nullable
. Indeed, if nullable=False
in the model decorator, then Optional(OptType)
will return Optional(Maybe(OptType), None)
directly, without checking for nullability conditions for OptType
.
Optional Decorator
Some times you want to define a model such that every attribute is optional. These are the so-called optional models and define a type OPTIONAL
. You can quickly create an optional model by making use of the @optional
decorator:
1from typed import SomeType, OtherType
2from typed.models import optional
3
4@optional
5class MyModel:
6 some_attr: SomeType
7 other_attr: OtherType=default_value
8 ...
9
10print(isinstance(MyModel, OPTIONAL)) # returns True
The above is equivalent to:
1from typed import SomeType, OtherType
2from typed.models import model, Optional
3
4@model
5class MyModel:
6 some_attr: Optional(SomeType)
7 other_attr: Optional(OtherType, default_value)
8 ...
While using
@optional
, you can control the behavior of the single argument case ofOptional
by making use of the variablenullable
. In other words,@optional(nullable=False)
implements the above, but with@model(nullable=False)
.
You can also use optional
not to define an optional model, but to turn an already existing model into an optional model:
1from typed import optional
2from some.where import MyModel
3
4OptionalModel = optional(MyModel, nullable=False).
5
6print(isinstance(MyModel, OPTIONAL)) # returns True
The decorator
@optional
preserves the model type. Indeed, ifisinstance(MyModel, EXACT)
isTrue
, thenisinstance(optional(MyModel), EXACT)
isTrue
as well, and similarly for the other model types.
Mandatory Attributes
If an attribute in a model is not optional, it is mandatory. A model in which every attribute is mandatory is a mandatory model, which constitute a type MANDATORY
.
Analogously to the optional case, there is a decorator @mandatory
. If used in the creation of a model, it ignores any introduction of the Optional
directive or any default value:
1from typed import SomeType, OtherType, AnotherType
2from typed.models import mandatory
3
4@mandatory
5class MyModel:
6 some_attr: SomeType
7 other_attr: OtherType=default_value
8 another_attr: Optional(AnotherType)
9 ...
10
11print(isinstance(MyModel, MANDATORY)) # returns True
The above is equivalent to:
1from typed import SomeType, OtherType
2from typed.models import model
3
4@model
5class MyModel:
6 some_attr: SomeType
7 other_attr: OtherType
8 another_attr: AnotherType
9 ...
The function @mandatory
can also be used to turn any already existing model into a mandatory model preserving its model type:
1from typed import mandatory
2from some.where import MyModel
3
4OptionalModel = mandatory(MyModel).
5
6print(isinstance(MyModel, MANDATORY)) # returns True
In the above, if isinstance(MyModel, EXACT)
is True
, then isinstance(optional(MyModel), EXACT)
is True
as well, and similarly for the other model types.
Model Extension
A model can be created extending other models. This ensures that the new model have at least the attributes that are defined in the model is extended. This is the inheritance behavior of models.
While defining a new model from a model factory, the models that are being extended can be listed in a __extends__
variable in the factory:
1from typed import SomeType, OtherType, ...
2from typed.models import ModelFactory
3from some.where import SomeModel, OtherModel, ...
4
5MyModel = ModelFactory(
6 __extends__=[SomeModel, OtherModel, ...],
7 some_attr=SomeType,
8 other_attr=OtherType,
9 ...
10)
In the case of using model decorators, the extended models are passed in the variable extends
(in the example below, the same works for other model decorators):
1from typed import SomeType, OtherType, ...
2from typed.models import model
3from some.where import SomeModel, OtherModel, ...
4
5@model(extends=[SomeModel, OtherModel, ...])
6class MyModel:
7 some_attr=SomeType,
8 other_attr=OtherType,
9 ...
The variables
__extends__
andextends
accept not onlyList(MODEL)
type, but also other iterative types based on some model type, asTuple(MODEL)
,Set(MODEL)
or even justMODEL
if a single model will be extended.
Filtering Models
Model Attributes
The model types MODEL
, EXACT
, and so on, have certain properties, which define corresponding attributes to their models: the so-called model attributes. They are described below:
attribute |
meaning |
---|---|
.is_model |
True for MODEL instances |
.is_exact |
True for EXACT instances |
.is_ordered |
True for ORDERED instances |
.is_rigid |
True for RIGID instances |
.attrs |
dict of all attributes |
.optional_attrs |
dict of optional attributes |
.mandatory_attrs |
dict of mandatory attributes |
Model Operations
… TBA …