Skip to main content

Pydantic Models Styling Guide

This guide defines the standards and patterns for writing Pydantic models in the pan-scm-sdk project.

Table of Contents​

  1. File Structure
  2. Model Hierarchy
  3. Model Configuration
  4. Field Definitions
  5. Validators
  6. Container Validation
  7. Enum Types
  8. Supporting Models
  9. Import Organization
  10. Naming Conventions

File Structure​

Each resource's models should be in a single file within the appropriate category:

scm/models/
├── __init__.py
├── objects/ # address.py, tag.py, service.py, etc.
├── security/ # security_rule.py, anti_spyware_profile.py, etc.
├── network/ # nat_rules.py, ike_gateway.py, etc.
├── deployment/ # service_connections.py, remote_networks.py, etc.
├── mobile_agent/ # auth_settings.py, agent_versions.py, etc.
└── setup/ # folder.py, snippet.py, variable.py, etc.

Model Hierarchy​

Every resource should define four models:

  1. BaseModel - Common fields shared across all operations
  2. CreateModel - Fields for creating new resources (inherits from Base)
  3. UpdateModel - Fields for updating resources (inherits from Base, adds id)
  4. ResponseModel - Fields returned from API (inherits from Base, adds id and response-only fields)
class ResourceBaseModel(BaseModel):
"""Base model with common fields."""
pass

class ResourceCreateModel(ResourceBaseModel):
"""Model for creating new resources."""
pass

class ResourceUpdateModel(ResourceBaseModel):
"""Model for updating existing resources."""
id: UUID = Field(...)

class ResourceResponseModel(ResourceBaseModel):
"""Model for API responses."""
id: UUID = Field(...)
# Additional response-only fields

Model Configuration​

Always use ConfigDict with these settings:

from pydantic import BaseModel, ConfigDict

class ResourceBaseModel(BaseModel):
"""Base model for Resource resources."""

model_config = ConfigDict(
extra="forbid", # Reject unknown fields
populate_by_name=True, # Allow field aliases
)

Configuration Options​

OptionValuePurpose
extra"forbid"Reject fields not defined in model
populate_by_nameTrueAllow using field name or alias

Field Definitions​

Required Fields​

Use ... (Ellipsis) to mark required fields:

name: str = Field(
..., # required
description="The name of the resource",
max_length=63,
)

Optional Fields​

Use Optional[T] with default=None:

description: Optional[str] = Field(
default=None,
description="An optional description",
)

Field Parameters​

from pydantic import Field

# String with constraints
name: str = Field(
...,
description="Resource name",
max_length=63,
pattern=r"^[a-zA-Z0-9\-_. ]+$",
)

# UUID field
id: UUID = Field(
...,
description="Unique identifier",
examples=["123e4567-e89b-12d3-a456-426655440000"],
)

# Boolean with default
enabled: bool = Field(
default=True,
description="Whether the resource is enabled",
)

# List field
tags: Optional[List[str]] = Field(
default=None,
description="List of tags",
)

Common Field Patterns​

Container Fields (folder/snippet/device)​

folder: Optional[str] = Field(
None,
pattern=r"^[a-zA-Z0-9\-_. ]+$",
max_length=64,
description="The folder in which the resource is defined",
)
snippet: Optional[str] = Field(
None,
pattern=r"^[a-zA-Z0-9\-_. ]+$",
max_length=64,
description="The snippet in which the resource is defined",
)
device: Optional[str] = Field(
None,
pattern=r"^[a-zA-Z0-9\-_. ]+$",
max_length=64,
description="The device in which the resource is defined",
)

Name Field​

name: str = Field(
...,
description="The name of the resource",
max_length=63,
pattern=r"^[a-zA-Z0-9\-_. ]+$",
)

ID Field (for Update/Response models)​

id: UUID = Field(
...,
description="The unique identifier of the resource",
examples=["123e4567-e89b-12d3-a456-426655440000"],
)

Validators​

Field Validators​

Use @field_validator for single-field validation:

from pydantic import field_validator

@field_validator("name")
@classmethod
def validate_name(cls, v):
"""Validate that the name is not empty."""
if not v or v.strip() == "":
raise ValueError("Name cannot be empty")
return v

Enum Validation​

When a field must be one of specific values:

@field_validator("type")
@classmethod
def validate_type_enum(cls, v):
"""Validate that the type is one of the allowed values."""
allowed = ["ip-netmask", "ip-range", "fqdn", "ip-wildcard"]
if v not in allowed:
raise ValueError(f"type must be one of {allowed}, got {v}")
return v

Model Validators​

Use @model_validator for cross-field validation:

from pydantic import model_validator

@model_validator(mode="after")
def validate_ip_or_fqdn(self) -> "ResourceBaseModel":
"""Ensure exactly one of ip_netmask or fqdn is provided."""
if self.ip_netmask and self.fqdn:
raise ValueError("Cannot specify both ip_netmask and fqdn")
if not self.ip_netmask and not self.fqdn:
raise ValueError("Must specify either ip_netmask or fqdn")
return self

Container Validation​

For resources requiring exactly one container (folder/snippet/device):

Method 1: Class Method with model_validate Override​

class ResourceBaseModel(BaseModel):
folder: Optional[str] = Field(None, ...)
snippet: Optional[str] = Field(None, ...)
device: Optional[str] = Field(None, ...)

@classmethod
def validate_container_type(cls, values):
"""Validate that exactly one container is provided."""
container_fields = [
values.get("folder"),
values.get("snippet"),
values.get("device")
]
set_count = sum(1 for v in container_fields if v is not None)
if set_count != 1:
raise ValueError(
"Exactly one of 'folder', 'snippet', or 'device' must be provided."
)
return values


class ResourceCreateModel(ResourceBaseModel):
@classmethod
def model_validate(cls, value):
"""Validate model and ensure container type is valid."""
model = super().model_validate(value)
cls.validate_container_type(model.__dict__)
return model

Method 2: Model Validator​

@model_validator(mode="after")
def validate_container(self) -> "ResourceBaseModel":
"""Ensure exactly one container is specified."""
containers = [self.folder, self.snippet, self.device]
specified = sum(1 for c in containers if c is not None)

if specified != 1:
raise ValueError(
"Exactly one of folder, snippet, or device must be specified"
)
return self

Enum Types​

Using Literal for Simple Enums​

from typing import Literal

action: Literal["allow", "deny", "drop"] = Field(
...,
description="The action to take",
)

Using Enum Class​

For reusable enums:

from enum import Enum

class ActionType(str, Enum):
"""Valid action types."""
ALLOW = "allow"
DENY = "deny"
DROP = "drop"

action: ActionType = Field(
...,
description="The action to take",
)

Documenting Enum Values​

Always document valid values in field description or validator:

type: str = Field(
...,
description="Variable type: percent, count, ip-netmask, zone, ip-range, "
"ip-wildcard, device-priority, device-id, egress-max, as-number, "
"fqdn, port, link-tag, group-id, rate, router-id, qos-profile, timer",
)

Supporting Models​

For nested structures, define supporting models:

class FolderReference(BaseModel):
"""Reference to a folder."""

model_config = ConfigDict(
extra="forbid",
populate_by_name=True,
)

id: UUID = Field(..., description="The UUID of the folder")
name: str = Field(..., description="The name of the folder")

@field_validator("name")
@classmethod
def validate_name(cls, value):
"""Validate that the name is not empty."""
if not value or value.strip() == "":
raise ValueError("Folder name cannot be empty")
return value


class ResourceResponseModel(ResourceBaseModel):
folders: Optional[List[FolderReference]] = Field(
default=None,
description="Folders the resource is applied to",
)

Import Organization​

"""Models for Resource in Palo Alto Networks' Strata Cloud Manager.

This module defines the Pydantic models used for creating, updating, and
representing Resource objects in the Strata Cloud Manager.
"""

from enum import Enum
from typing import List, Literal, Optional, Union
from uuid import UUID

from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator

Naming Conventions​

ElementConventionExample
Base model{Resource}BaseModelAddressBaseModel
Create model{Resource}CreateModelAddressCreateModel
Update model{Resource}UpdateModelAddressUpdateModel
Response model{Resource}ResponseModelAddressResponseModel
Enum class{Name}Type or {Name}EnumActionType, ProtocolEnum
Supporting modelDescriptive nameFolderReference, ThreatEntry
File namesnake_case, singularaddress.py, security_rule.py

Complete Example​

"""Models for Variable in Palo Alto Networks' Strata Cloud Manager.

This module defines the Pydantic models used for creating, updating, and
representing Variable resources in the Strata Cloud Manager.
"""

from typing import List, Optional
from uuid import UUID

from pydantic import BaseModel, ConfigDict, Field, field_validator


class VariableBaseModel(BaseModel):
"""Base model for Variable resources."""

model_config = ConfigDict(
extra="forbid",
populate_by_name=True,
)

name: str = Field(
...,
description="The name of the variable",
max_length=63,
)
type: str = Field(
...,
description="The variable type",
)
value: str = Field(
...,
description="The value of the variable",
)
description: Optional[str] = Field(
default=None,
description="An optional description of the variable",
)
folder: Optional[str] = Field(
None,
pattern=r"^[a-zA-Z0-9\-_. ]+$",
max_length=64,
description="The folder in which the variable is defined",
)
snippet: Optional[str] = Field(
None,
pattern=r"^[a-zA-Z0-9\-_. ]+$",
max_length=64,
description="The snippet in which the variable is defined",
)
device: Optional[str] = Field(
None,
pattern=r"^[a-zA-Z0-9\-_. ]+$",
max_length=64,
description="The device in which the variable is defined",
)

@field_validator("type")
@classmethod
def validate_type_enum(cls, v):
"""Validate that the type is one of the allowed values."""
allowed = [
"percent", "count", "ip-netmask", "zone", "ip-range",
"ip-wildcard", "device-priority", "device-id", "egress-max",
"as-number", "fqdn", "port", "link-tag", "group-id",
"rate", "router-id", "qos-profile", "timer",
]
if v not in allowed:
raise ValueError(f"type must be one of {allowed}, got {v}")
return v

@classmethod
def validate_container_type(cls, values):
"""Validate that exactly one container is provided."""
container_fields = [
values.get("folder"),
values.get("snippet"),
values.get("device")
]
set_count = sum(1 for v in container_fields if v is not None)
if set_count != 1:
raise ValueError(
"Exactly one of 'folder', 'snippet', or 'device' must be provided."
)
return values


class VariableCreateModel(VariableBaseModel):
"""Model for creating new Variable resources."""

@classmethod
def model_validate(cls, value):
"""Validate model and ensure container type is valid."""
model = super().model_validate(value)
cls.validate_container_type(model.__dict__)
return model


class VariableUpdateModel(VariableBaseModel):
"""Model for updating existing Variable resources."""

id: UUID = Field(
...,
description="The unique identifier of the variable",
)

@classmethod
def model_validate(cls, value):
"""Validate model and ensure container type is valid."""
model = super().model_validate(value)
cls.validate_container_type(model.__dict__)
return model


class VariableResponseModel(VariableBaseModel):
"""Model for Variable responses from the API."""

id: UUID = Field(
...,
description="The unique identifier of the variable",
examples=["123e4567-e89b-12d3-a456-426655440000"],
)
overridden: Optional[bool] = Field(
default=None,
description="Is the variable overridden?",
)
labels: Optional[List[str]] = Field(
default=None,
description="Labels assigned to the variable",
)
parent: Optional[str] = Field(
default=None,
description="The parent folder or container",
)