Serialization (pyobs.utils.serialization)

The robotic subsystem uses pydantic models rather than Object subclasses for its data and logic objects. This page explains the two base classes that make this work and how they fit into the broader pyobs configuration system.

Why pydantic models?

Object is the right base class for anything with an async lifecycle — modules, hardware drivers, background tasks. But many robotic objects are pure data or stateless logic: a Task is just a validated config dict; a Constraint is a function; a Script runs once and is discarded. These map naturally onto pydantic models, which give you field validation, type coercion, and YAML round-tripping for free, without the overhead of managing open()/close() lifecycles.

The two base classes in pyobs.utils.serialization bridge pydantic’s validation machinery with pyobs’s runtime context system.

BaseModel

BaseModel is the pydantic equivalent of Object for non-polymorphic models. It adds the five runtime context attributes as PrivateAttr fields and injects them from a validation context if one is present:

from pyobs.utils.serialization import BaseModel

class Task(BaseModel):
    name: str
    duration: float
    priority: float = 1.0

Task, Project, and Observation all use BaseModel directly — they are always instantiated as their concrete type, never dispatched via a class: key.

The runtime context (_comm, _vfs, _observer, _timezone, _location) is populated in two ways:

  1. Via validation context — when pyobs_model_validate() is called from within an Object, it passes the object’s context to model_validate explicitly:

    script = self.pyobs_model_validate(Script, self.script, by_alias=True)
    

    Pydantic propagates this context down the full validation tree, so nested PolymorphicBaseModel instances (e.g. a TargetPicker inside a Script) also receive it automatically via the _inject_context_into_children validator.

  2. Via ``pyobs_model_validate`` after the fact — when BaseModel is used in a context where no validation context is available, pyobs_model_validate stamps the private attrs directly onto the model after validation.

PolymorphicBaseModel

PolymorphicBaseModel extends BaseModel for cases where the concrete type is not known at parse time — the class: key in the YAML selects it at runtime. Constraint, Merit, Target, and Script are all polymorphic base classes.

It adds two model validators:

Deserialization (retrieve_class_on_deserialization) — a wrap validator that intercepts the incoming dict, reads the class: key, resolves the class, and delegates to that class’s own model_validate. The validation context is forwarded so runtime injection reaches the concrete type:

# this YAML block:
constraints:
  - class: pyobs.robotic.scheduler.constraints.AirmassConstraint
    max_airmass: 2.0

# causes pydantic to call:
AirmassConstraint.model_validate({"max_airmass": 2.0}, context=...)

Serialization (inject_class_on_serialization) — a wrap serializer that adds the class: key back into the serialised dict, so that a model_dump() followed by model_validate() round-trips correctly:

constraint = AirmassConstraint(max_airmass=2.0)
d = constraint.model_dump()
# → {"class": "pyobs.robotic.scheduler.constraints.AirmassConstraint", "max_airmass": 2.0}

PrivateAttrMixin

The runtime context properties (comm, vfs, observer, location, timezone) are defined on PrivateAttrMixin, which both Object and BaseModel inherit from. This ensures that scripts, constraints, merits, and targets all expose the same property interface as full Object subclasses, even though they are pydantic models:

class ObserveScript(Script):
    async def run(self, data):
        camera = await self.comm.proxy("camera", ICamera)  # same as in any Module
        image = await self.vfs.read_image("/cache/last.fits")

See Objects (pyobs.object) for the full list of properties.

API reference

class BaseModel[source]

Bases: BaseModel, PrivateAttrMixin

Pydantic base model for pyobs classes that need to be serialized.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

model_config = {'arbitrary_types_allowed': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_post_init(context: Any, /) None

This function is meant to behave like a BaseModel method to initialize private attributes.

It takes context as an argument since that’s what pydantic-core passes when calling it.

Parameters:
  • self – The BaseModel instance.

  • context – The context.

class PolymorphicBaseModel[source]

Bases: BaseModel

Pydantic base model for pyobs sub classes that need to be serialized.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

model_config = {'arbitrary_types_allowed': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

model_post_init(context: Any, /) None

This function is meant to behave like a BaseModel method to initialize private attributes.

It takes context as an argument since that’s what pydantic-core passes when calling it.

Parameters:
  • self – The BaseModel instance.

  • context – The context.

classmethod retrieve_class_on_deserialization(value: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo) Any[source]

Get the correct class for this model and run model_validate on that class with the current context.

class PrivateAttrMixin[source]
pyobs_model_validate(cls: type[PydanticModel], *args: Any, **kwargs: Any) PydanticModel[source]

Validate a pydantic model with additional fields.