Skip to content

How to Create Custom Types

This guide shows advanced techniques for creating custom types and extending pyvider.cty's type system.

๐Ÿค– AI-Generated Content

This documentation was generated with AI assistance and is still being audited. Some, or potentially a lot, of this information may be inaccurate. Learn more.

When to Create Custom Types

Consider custom types when you need: - Domain-specific validation logic - Reusable type compositions - Complex validation rules - Custom serialization behavior

Type Composition

The simplest way to create "custom" types is through composition:

from pyvider.cty import CtyObject, CtyString, CtyNumber, CtyBool, CtyList

# Create reusable composite types
def EmailType():
    """String type for email addresses."""
    return CtyString()  # In practice, add validation

def URLType():
    """String type for URLs."""
    return CtyString()

def PositiveNumberType():
    """Number type for positive values."""
    return CtyNumber()  # In practice, add validation

# Use them in schemas
user_type = CtyObject(
    attribute_types={
        "name": CtyString(),
        "email": EmailType(),
        "website": URLType(),
        "age": PositiveNumberType()
    }
)

Factory Functions

Create factory functions for common type patterns:

def TimestampType():
    """ISO 8601 timestamp as string."""
    return CtyString()

def IDType():
    """Unique identifier as string."""
    return CtyString()

def EnumType(*allowed_values):
    """Constrained string enum."""
    # Note: Real implementation would validate against allowed_values
    return CtyString()

# Usage
resource_type = CtyObject(
    attribute_types={
        "id": IDType(),
        "status": EnumType("active", "inactive", "pending"),
        "created_at": TimestampType()
    }
)

Validation Wrappers

Wrap types with additional validation:

from pyvider.cty import CtyString, CtyNumber
from pyvider.cty.exceptions import CtyValidationError
import re

class EmailString:
    """Email validated string type."""

    def __init__(self):
        self.base_type = CtyString()

    def validate(self, value):
        # First validate as string
        cty_value = self.base_type.validate(value)

        # Then check email format
        email_pattern = r'^[\w\.-]+@[\w\.-]+\.\w+$'
        if not re.match(email_pattern, cty_value.raw_value):
            raise CtyValidationError(f"Invalid email format: {value}")

        return cty_value

# Usage
email_type = EmailString()
valid_email = email_type.validate("user@example.com")

Range-Constrained Numbers

class RangeNumber:
    """Number type with min/max constraints."""

    def __init__(self, min_value=None, max_value=None):
        self.base_type = CtyNumber()
        self.min_value = min_value
        self.max_value = max_value

    def validate(self, value):
        cty_value = self.base_type.validate(value)
        num = float(cty_value.raw_value)

        if self.min_value is not None and num < self.min_value:
            raise CtyValidationError(
                f"Value {num} is less than minimum {self.min_value}"
            )

        if self.max_value is not None and num > self.max_value:
            raise CtyValidationError(
                f"Value {num} is greater than maximum {self.max_value}"
            )

        return cty_value

# Usage
age_type = RangeNumber(min_value=0, max_value=150)
port_type = RangeNumber(min_value=1, max_value=65535)

person = CtyObject(
    attribute_types={
        "name": CtyString(),
        "age": age_type
    }
)

Pattern-Validated Strings

class PatternString:
    """String type with regex pattern validation."""

    def __init__(self, pattern, error_message=None):
        self.base_type = CtyString()
        self.pattern = re.compile(pattern)
        self.error_message = error_message or f"Must match pattern: {pattern}"

    def validate(self, value):
        cty_value = self.base_type.validate(value)

        if not self.pattern.match(cty_value.raw_value):
            raise CtyValidationError(self.error_message)

        return cty_value

# Usage
phone_type = PatternString(
    r'^\+?1?\d{10,14}$',
    "Must be a valid phone number"
)

zip_code_type = PatternString(
    r'^\d{5}(-\d{4})?$',
    "Must be a valid US ZIP code"
)

contact = CtyObject(
    attribute_types={
        "phone": phone_type,
        "zip": zip_code_type
    }
)

Length-Constrained Collections

from pyvider.cty import CtyList

class SizedList:
    """List type with size constraints."""

    def __init__(self, element_type, min_length=None, max_length=None):
        self.base_type = CtyList(element_type=element_type)
        self.min_length = min_length
        self.max_length = max_length

    def validate(self, value):
        cty_value = self.base_type.validate(value)

        length = len(value)

        if self.min_length is not None and length < self.min_length:
            raise CtyValidationError(
                f"List length {length} is less than minimum {self.min_length}"
            )

        if self.max_length is not None and length > self.max_length:
            raise CtyValidationError(
                f"List length {length} is greater than maximum {self.max_length}"
            )

        return cty_value

# Usage
tags_type = SizedList(CtyString(), min_length=1, max_length=10)

Conditional Validation

class ConditionalObject:
    """Object type with conditional field requirements."""

    def __init__(self, base_schema, conditionals):
        self.base_schema = base_schema
        self.conditionals = conditionals

    def validate(self, value):
        # First validate against base schema
        cty_value = self.base_schema.validate(value)

        # Then check conditional rules
        for condition, required_fields in self.conditionals:
            if condition(value):
                for field in required_fields:
                    if field not in value or value[field] is None:
                        raise CtyValidationError(
                            f"Field '{field}' is required when condition is met"
                        )

        return cty_value

# Usage
payment_schema = CtyObject(
    attribute_types={
        "method": CtyString(),
        "credit_card": CtyString(),
        "bank_account": CtyString()
    },
    optional_attributes={"credit_card", "bank_account"}
)

conditionals = [
    (lambda v: v["method"] == "card", ["credit_card"]),
    (lambda v: v["method"] == "bank", ["bank_account"])
]

payment_type = ConditionalObject(payment_schema, conditionals)

Type Registries

Organize custom types in registries:

class TypeRegistry:
    """Registry for custom types."""

    def __init__(self):
        self.types = {}

    def register(self, name, type_factory):
        """Register a type factory."""
        self.types[name] = type_factory

    def get(self, name):
        """Get a registered type."""
        if name not in self.types:
            raise ValueError(f"Unknown type: {name}")
        return self.types[name]()

    def create_object(self, schema_dict):
        """Create object from schema dictionary."""
        attribute_types = {}
        for attr, type_name in schema_dict.items():
            attribute_types[attr] = self.get(type_name)

        return CtyObject(attribute_types)

# Usage
registry = TypeRegistry()
registry.register("email", EmailString)
registry.register("age", lambda: RangeNumber(0, 150))
registry.register("phone", lambda: PatternString(r'^\+?1?\d{10,14}$'))

# Create types from registry
user_schema = registry.create_object({
    "name": "string",
    "email": "email",
    "age": "age"
})

Best Practices

  1. Compose before creating: Use existing types when possible
  2. Validate incrementally: Build on base type validation
  3. Provide clear errors: Make validation failures informative
  4. Document constraints: Clearly document what your types enforce
  5. Test thoroughly: Custom types need comprehensive tests
  6. Consider reusability: Design for use across your codebase

Common Patterns

Domain-Specific Types

# Application-specific types
class UserIDType:
    """User ID with format validation."""
    def __init__(self):
        self.base_type = PatternString(r'^user_[a-f0-9]{16}$')

    def validate(self, value):
        return self.base_type.validate(value)

class ResourceARNType:
    """AWS ARN type."""
    def __init__(self):
        self.base_type = PatternString(r'^arn:aws:[a-z0-9-]+:[a-z0-9-]*:\d+:.+$')

    def validate(self, value):
        return self.base_type.validate(value)

Versioned Schemas

def UserSchemaV1():
    """User schema version 1."""
    return CtyObject(
        attribute_types={
            "name": CtyString(),
            "email": EmailString()
        }
    )

def UserSchemaV2():
    """User schema version 2 with additional fields."""
    return CtyObject(
        attribute_types={
            "name": CtyString(),
            "email": EmailString(),
            "phone": PatternString(r'^\+?1?\d{10,14}$'),
            "created_at": TimestampType()
        }
    )

# Select schema based on version
def get_user_schema(version):
    schemas = {
        1: UserSchemaV1,
        2: UserSchemaV2
    }
    return schemas[version]()

Next Steps