Validating Network Intent with Pydantic, JSON Schema, YAML, and Jinja2
Network automation usually breaks before it touches a router. The failure is often bad input data: a missing VLAN ID, an interface name typo, a wrong site slug, or a template variable that nobody supplied.
That is why the first useful NetDevOps CI job is not "push config". It is "prove that the intended state is valid".
The netdevops_cicd images include the tooling needed for this kind of pipeline:
pydanticfor Python data models.jsonschemafor validating JSON contracts.pyyamlfor YAML inventory and intent files.jinja2for rendering configs.pytestfor repeatable tests.yamllintfor basic YAML hygiene.jmespathfor querying nested data structures.
Example Intent Data
Example data/sites.yml:
sites:
- name: ams1
vlans:
- id: 10
name: users
- id: 20
name: servers
interfaces:
- name: Ethernet1
description: uplink-to-core
enabled: true
That file looks simple, but it already has rules:
- VLAN IDs must be between 1 and 4094.
- Interface names must not be empty.
- Descriptions should be strings.
- The site name should follow your naming standard.
Validate with Pydantic
Create tests/test_intent_models.py:
from pathlib import Path
import yaml
from pydantic import BaseModel, Field
class Vlan(BaseModel):
id: int = Field(ge=1, le=4094)
name: str = Field(min_length=1)
class Interface(BaseModel):
name: str = Field(min_length=1)
description: str | None = None
enabled: bool = True
class Site(BaseModel):
name: str = Field(pattern=r"^[a-z0-9-]+$")
vlans: list[Vlan]
interfaces: list[Interface]
def test_site_intent_is_valid():
data = yaml.safe_load(Path("data/sites.yml").read_text())
sites = [Site.model_validate(item) for item in data["sites"]]
assert sites
Run it in CI:
jobs:
validate-intent:
runs-on: ubuntu-latest
container:
image: bsmeding/netdevops_cicd_ubuntu:latest
steps:
- uses: actions/checkout@v5
- run: yamllint data/
- run: pytest tests/test_intent_models.py -vv
Validate External Contracts with JSON Schema
Pydantic is great when your automation code owns the model. JSON Schema is useful when you want a contract that other systems can validate too.
Example schemas/vlan.json:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"required": ["id", "name"],
"properties": {
"id": {
"type": "integer",
"minimum": 1,
"maximum": 4094
},
"name": {
"type": "string",
"minLength": 1
}
}
}
Example validation:
import json
from pathlib import Path
import yaml
from jsonschema import Draft202012Validator
def test_vlan_schema():
schema = json.loads(Path("schemas/vlan.json").read_text())
validator = Draft202012Validator(schema)
data = yaml.safe_load(Path("data/sites.yml").read_text())
for site in data["sites"]:
for vlan in site["vlans"]:
validator.validate(vlan)
Render Jinja2 Only After Data Passes
Once the input data is valid, render templates:
hostname {{ site.name }}
{% for vlan in site.vlans %}
vlan {{ vlan.id }}
name {{ vlan.name }}
{% endfor %}
Example render test:
from jinja2 import Environment, FileSystemLoader, StrictUndefined
def test_template_renders(site):
env = Environment(
loader=FileSystemLoader("templates"),
undefined=StrictUndefined,
trim_blocks=True,
lstrip_blocks=True,
)
template = env.get_template("vlans.j2")
output = template.render(site=site)
assert "vlan 10" in output
StrictUndefined matters. It makes missing variables fail the test instead of producing half-rendered configs.
Practical Tips
- Validate YAML before rendering templates.
- Use Pydantic when Python owns the model.
- Use JSON Schema when other systems need the same contract.
- Use
StrictUndefinedin Jinja2 CI tests. - Store rendered configs as CI artifacts for review.
Summary
Good CI for network automation starts with intent validation. Pydantic, JSON Schema, PyYAML, Jinja2, pytest, and yamllint catch the boring mistakes early, before they become failed changes or emergency rollbacks.