Validation¶
Validation is the process of checking whether raw Python data conforms to a defined type schema. In pyvider.cty, validation is the gateway between untrusted or untyped data and type-safe CtyValue objects.
๐ค 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.
How Validation Works¶
Every CtyType provides a validate() method that:
- Accepts raw Python data (dicts, lists, primitives, etc.)
- Checks conformance against the type schema
- Returns a CtyValue if valid
- Raises a CtyValidationError if invalid
from pyvider.cty import CtyString, CtyValidationError
string_type = CtyString()
# Valid: returns CtyValue
result = string_type.validate("hello")
print(result.raw_value) # "hello"
# Invalid: raises CtyValidationError
try:
string_type.validate(123)
except CtyValidationError as e:
print(f"Error: {e}") # Error: expected CtyString, got int
Validation vs Direct Construction¶
You should always prefer validate() over direct CtyValue construction:
# โ
RECOMMENDED: Use validate()
value = CtyString().validate("hello")
# โ AVOID: Direct construction (internal API)
# from pyvider.cty.values import CtyValue
# value = CtyValue(...) # Don't do this!
The validate() method provides:
- Type checking and coercion
- Clear error messages
- Consistent behavior across all types
- Protection against malformed data
Validation Rules by Type¶
Primitives¶
CtyString
- Accepts: str values
- Rejects: Non-string types
- Special handling: Unicode normalization (NFC)
CtyNumber
- Accepts: int, float, Decimal
- Rejects: Non-numeric types
- Special handling: Preserves precision with Decimal
CtyBool
- Accepts: bool values (True, False)
- Rejects: Non-boolean types (including truthy/falsy values like 1, 0, "")
Collections¶
CtyList
- Accepts: list or other iterables
- Element validation: Each element must match element_type
- Preserves: Order
CtySet
- Accepts: set, frozenset, or iterables
- Element validation: Each element must match element_type
- Removes: Duplicates (enforces set semantics)
CtyMap
- Accepts: dict with string keys
- Element validation: All values must match element_type
- Key requirement: Keys must be strings
Structural Types¶
CtyObject
- Accepts: dict with specific attributes
- Attribute validation: Each attribute validated against its type
- Optional attributes: Missing optional attributes become null values
- Required attributes: Must be present (unless optional)
CtyTuple
- Accepts: list, tuple, or iterables
- Element validation: Each position validated against its element type
- Length requirement: Must match declared element count
Recursive Validation¶
Validation works recursively through nested structures:
from pyvider.cty import CtyObject, CtyList, CtyString, CtyNumber
# Nested schema
company_type = CtyObject(
attribute_types={
"name": CtyString(),
"employees": CtyList(
element_type=CtyObject(
attribute_types={
"name": CtyString(),
"salary": CtyNumber()
}
)
)
}
)
# Validation descends through the structure
company_data = {
"name": "Acme Corp",
"employees": [
{"name": "Alice", "salary": 100000},
{"name": "Bob", "salary": 95000}
]
}
company = company_type.validate(company_data)
# If Bob's salary was "95000" (string), validation would fail
# at the path: employees[1].salary
Validation Context and Depth Limits¶
pyvider.cty tracks validation depth to prevent infinite recursion:
from pyvider.cty.context import deeper_validation, MAX_VALIDATION_DEPTH
# Context tracks current validation depth
with deeper_validation():
# Validation depth increased by 1
pass
# Maximum depth: 500 (configurable via MAX_VALIDATION_DEPTH)
This protection prevents stack overflow with: - Circular references (if raw data contains them) - Extremely deep nesting - Malicious input designed to cause resource exhaustion
Error Handling¶
Validation errors provide detailed context:
from pyvider.cty import CtyObject, CtyString, CtyNumber
from pyvider.cty.exceptions import CtyAttributeValidationError
user_type = CtyObject(
attribute_types={
"name": CtyString(),
"age": CtyNumber()
}
)
try:
user_type.validate({
"name": "Alice",
"age": "thirty" # Wrong type!
})
except CtyAttributeValidationError as e:
print(e.attribute_name) # "age"
print(e.path) # Path to error location
print(e) # Full error message
Exception Hierarchy¶
CtyValidationError (base)
โโโ CtyTypeMismatchError
โโโ CtyAttributeValidationError
โโโ CtyListValidationError
โโโ CtyMapValidationError
โโโ CtySetValidationError
โโโ CtyTupleValidationError
Catch specific exceptions for targeted error handling:
try:
value = schema.validate(data)
except CtyAttributeValidationError as e:
# Handle object attribute errors
log.error(f"Attribute {e.attribute_name} is invalid")
except CtyListValidationError as e:
# Handle list validation errors
log.error(f"List validation failed at index {e.index}")
except CtyValidationError as e:
# Catch-all for other validation errors
log.error(f"Validation failed: {e}")
Validation Best Practices¶
1. Validate at System Boundaries¶
Always validate data when it enters your system:
def handle_api_request(request_data):
"""Validate incoming API data."""
try:
validated = request_schema.validate(request_data)
return process_request(validated)
except CtyValidationError as e:
return {"error": str(e)}, 400
2. Use Specific Error Handling¶
Catch specific validation errors for better error messages:
try:
config = config_schema.validate(raw_config)
except CtyAttributeValidationError as e:
print(f"Invalid configuration: {e.attribute_name} is incorrect")
except CtyValidationError as e:
print(f"Configuration validation failed: {e}")
3. Build Schemas Incrementally¶
Define sub-schemas and compose them:
# Define reusable schemas
address_type = CtyObject(
attribute_types={
"street": CtyString(),
"city": CtyString(),
"zip": CtyString()
}
)
# Compose into larger schemas
person_type = CtyObject(
attribute_types={
"name": CtyString(),
"address": address_type # Reuse
}
)
4. Document Optional Fields¶
Make it clear which fields are optional:
# Clear documentation of optional fields
user_type = CtyObject(
attribute_types={
"username": CtyString(), # Required
"email": CtyString(), # Required
"phone": CtyString(), # Optional
"bio": CtyString() # Optional
},
optional_attributes={"phone", "bio"}
)
5. Test Edge Cases¶
Test validation with edge cases:
def test_user_validation():
"""Test user validation with various inputs."""
# Test valid data
valid_user = user_type.validate({"username": "alice", "email": "[email protected]"})
assert valid_user is not None
# Test missing required field
with pytest.raises(CtyValidationError):
user_type.validate({"username": "bob"}) # Missing email
# Test wrong type
with pytest.raises(CtyValidationError):
user_type.validate({"username": 123, "email": "[email protected]"})
Performance Considerations¶
Validation has computational cost:
- Type checking - Every value is type-checked
- Recursion - Nested structures validated recursively
- Immutable construction - Creates new immutable values
For performance-critical paths:
# Validate once at the boundary
config = config_schema.validate(raw_config)
# Reuse the validated value
for _ in range(1000):
# Don't validate again inside the loop
process(config)
Integration with Type Conversion¶
Validation and type conversion work together:
from pyvider.cty import convert, CtyString, CtyNumber
# Validation: Strict type checking
number_type = CtyNumber()
number_type.validate("123") # โ Raises error: expected number, got str
# Conversion: Flexible type transformation
string_val = CtyString().validate("123")
number_val = convert(string_val, CtyNumber()) # โ
Converts "123" to 123
See Also¶
- Type Conversion - Converting between types
- How-To: Validate Data - Practical validation patterns
- Error Handling - Handling validation errors