Skip to content

Hello World

YANG Model

Let's take a simple yang model for a "Hello World" example. This model allows specifying a simple endpoint containing an address and port. An optional description can be defined.

my-endpoint.yang
module my-endpoint {
  namespace
  "http://pydantify.github.io/ns/yang/pydantify-endpoint";

  prefix "ep";

  description 'Example demonstarating leafref nodes';

  typedef port {
    type uint16 {
        range "1 .. 65535";
      }
  }

  container endpoint {
    description "Definition of a endpoint";

    leaf address {
      type string;
      description "Endpoint address. IP or FQDN";
      mandatory true;
    }
    leaf port {
      type port;
      description "Port number between 1 and 65535";
      mandatory true;
    }
    leaf description {
      type string;
      description "Endpoint description";
    }
  }
}

Using pyang the model can be validated and displayed as a tree.

pyang -f tree my-endpoint.yang
module: my-endpoint
  +--rw endpoint
     +--rw address        string
     +--rw port           port
     +--rw description?   string

Create pydantic model

Pydantify can now convert the YANG model into a pydantic one.

$ pydantify examples/hello_world/my-endpoint.yang
[INFO] /workspaces/pydantify/pydantify/plugins/pydantic_plugin.py:41 (emit): Output model generated in 0.049s.

The generated module will be in the file out/out.py. We can move and rename it to endpoint.py.

endpoint.py
from __future__ import annotations

from typing import Optional

from pydantic import BaseModel, ConfigDict, Field, RootModel
from typing_extensions import Annotated


class EndpointContainer(BaseModel):
    """
    Definition of a endpoint
    """

    model_config = ConfigDict(
        populate_by_name=True,
        regex_engine="python-re",
    )
    address: Annotated[str, Field(alias='my-endpoint:address', title='AddressLeaf')]
    """
    Endpoint address. IP or FQDN
    """
    port: Annotated[
        int, Field(alias='my-endpoint:port', ge=1, le=65535, title='PortLeaf')
    ]
    """
    Port number between 1 and 65535
    """
    description: Annotated[
        Optional[str], Field(alias='my-endpoint:description', title='DescriptionLeaf')
    ] = None
    """
    Endpoint description
    """


class Model(BaseModel):
    """
    Initialize an instance of this class and serialize it to JSON; this results in a RESTCONF payload.

    ## Tips
    Initialization:
    - all values have to be set via keyword arguments
    - if a class contains only a `root` field, it can be initialized as follows:
        - `member=MyNode(root=<value>)`
        - `member=<value>`

    Serialziation:
    - `exclude_defaults=True` omits fields set to their default value (recommended)
    - `by_alias=True` ensures qualified names are used (necessary)
    """

    model_config = ConfigDict(
        populate_by_name=True,
        regex_engine="python-re",
    )
    endpoint: Annotated[
        Optional[EndpointContainer], Field(alias='my-endpoint:endpoint')
    ] = None


if __name__ == "__main__":
    model = Model(
        # <Initialize model here>
    )

    restconf_payload = model.model_dump_json(
        exclude_defaults=True, by_alias=True, indent=2
    )

    print(f"Generated output: {restconf_payload}")

    # Send config to network device:
    # from pydantify.utility import restconf_patch_request
    # restconf_patch_request(url='...', user_pw_auth=('usr', 'pw'), data=restconf_payload)

Using the model

The model can now be used as any other pydantic python model, and pydantify is not required if we don't use helper functions from pydantify.

The model can be imported, and Python objects can be created. The IDE (like Visual Studio Code) will offer code completion.

create_json.py
2
3
4
5
6
7
8
from endpoint import Model, EndpointContainer

port = 8080
host = "localhost"

endpoint1 = Model(endpoint=EndpointContainer(address=host, port=port))
json_output = endpoint1.model_dump_json(indent=2, exclude_none=True)

After creating the objects port and host, the model is instantiated with the passed objects. The pydantic model object can generate JSON directly by calling .json().

By default, the JSON output will include all values. Using the argument exlude_defaults=True will not show these values. In this example, the description leaf is optional and has, therefore, a default value of Null.

create_json.py
json_output = endpoint1.model_dump_json(exclude_defaults=True, by_alias=True, indent=2)

With the option by_alias the JSON includes the YANG module name in the keys.

endpoint1_json_exclude_default_and_by_alias.json
{
  "my-endpoint:endpoint": {
    "my-endpoint:address": "localhost",
    "my-endpoint:port": 8080
  }
}

Model objects containing only a root field can be created automatically. So, instead of creating a port object, specify the value in the argument of the EndpointContainer creation. Pydantic creates objects automatically in the background. More information can be found in the pydantic documentation for Custom Root Types

create_json.py
endpoint2 = Model(
    endpoint=EndpointContainer(
        address="::1", port=2222, description="Port 2222 on localhost"
    )
)
endpoint2.json
{
  "my-endpoint:endpoint": {
    "my-endpoint:address": "::1",
    "my-endpoint:port": 2222,
    "my-endpoint:description": "Port 2222 on localhost"
  }
}

A Python dictionary can also be used to create nested model objects:

create_json.py
endpoint3 = Model(endpoint={"address": "172.0.0.1", "port": 9100})
endpoint3.json
{
  "my-endpoint:endpoint": {
    "my-endpoint:address": "172.0.0.1",
    "my-endpoint:port": 9100
  }
}

Using dictionary unpacking, the first level of the dictionary needs to match the field names of the model. Inside the model, also the alias name can be used.

create_json.py
endpoint4 = Model(
    **{"endpoint": {"my-endpoint:address": "localhost", "my-endpoint:port": 8000}}
)
endpoint4.json
{
  "my-endpoint:endpoint": {
    "my-endpoint:address": "localhost",
    "my-endpoint:port": 8000
  }
}

To be able to use the alias also on the top level, the static class functions parse_obj, parse_file, or parse_raw can be used:

create_json.py
endpoint5 = Model.model_validate(
    {
        "my-endpoint:endpoint": {
            "my-endpoint:address": "remote",
            "my-endpoint:port": 53,
        }
    }
)
endpoint5.json
{
  "my-endpoint:endpoint": {
    "my-endpoint:address": "remote",
    "my-endpoint:port": 53
  }
}