Concepts: Pydantic Integration & Custom Types¶
The Role of Pydantic¶
Pydantic provides data validation and settings management using Python type annotations. By integrating with Pydantic, Dracon leverages these capabilities for configuration:
- Schema Definition: Define the expected structure, types, and constraints of your configuration using Pydantic
BaseModel
s. - Validation: Automatically validate loaded configuration data against your models, catching errors like incorrect types or missing required fields early.
- Type Coercion: Pydantic handles converting types where appropriate (e.g., a string "42" in YAML can become an integer
42
if the model field isint
). - Defaults: Define default values directly in your Pydantic models.
- Construction: Dracon constructs instances of your Pydantic models from the validated YAML data.
How Integration Works¶
- Tagging (
!YourModelName
): You mark a YAML node with a tag corresponding to your Pydantic model name (e.g.,!DatabaseConfig
). - Context: You provide the Pydantic model class(es) to the
DraconLoader
via itscontext
dictionary (e.g.,context={'DatabaseConfig': DatabaseConfig}
). This allows Dracon to find the class when it encounters the tag. - YAML Construction: Dracon first parses the tagged YAML node and its children into basic Python types (typically a dictionary for a mapping node).
- Pydantic Validation: Dracon then passes this intermediate dictionary to Pydantic's
TypeAdapter
(effectively callingYourModel.model_validate(intermediate_dict)
). - Instance Creation: Pydantic validates the data against the model definition:
- Checks required fields are present.
- Coerces types according to field annotations.
- Applies default values for missing optional fields.
- Runs any custom validators defined on the model.
- If validation succeeds, Pydantic creates an instance of
YourModel
. - If validation fails, a
ValidationError
is raised, typically surfaced by Dracon.
- Result: The constructed and validated Pydantic model instance becomes the value associated with the key in the final configuration object returned by Dracon.
Tip
The type tag !YourModelName
will check for this class in the context dictionary. You can also use a full path to the type (e.g., !mypackage.models.Server
) if you prefer, which lifts the type-in-context requirement.
Note
You don't have to use pydantic models with Dracon. They just work out-of-the box. However, you can also use your custom types. By default Dracon will try to feed the dictionnary of the YAML node to the constructor of your custom type. If you want to use a different approach, you can define your own representer and constructor for your custom type. See ruamel.yaml's doc for more information on that.
# models.py
from pydantic import BaseModel, Field, validator
class Server(BaseModel):
host: str
port: int = 8080 # Pydantic default
@validator('port')
def port_must_be_positive(cls, v):
if v <= 0:
raise ValueError('Port must be positive')
return v
# config.yaml
server: !Server # Tag instructs Dracon to use the Server model
host: "api.example.com"
# Port omitted - Pydantic default 8080 will be used after validation
# main.py
import dracon as dr
from models import Server
loader = dr.DraconLoader(context={'Server': Server})
config = loader.load('config.yaml')
assert isinstance(config.server, Server)
assert config.server.host == "api.example.com"
assert config.server.port == 8080 # Default applied by Pydantic