Schemas and Contracts¶
Surety separates two concepts:
A schema is a Python class that defines the expected structure of data — fields, types, and constraints. Schemas inherit from
Dictionary(orFieldfor individual values). Schemas are transport-agnostic: the same schema can validate an API response, a database record, or a UI state.A contract adds communication semantics on top of a schema — such as an API method and endpoint path, a database table reference, or an event name. Contracts live in extension packages like
surety-apiandsurety-db.
Defining a Schema¶
Inherit from Dictionary and declare fields as class attributes:
from surety import Dictionary, String, Int, Bool
class Widget(Dictionary):
WidgetId = Int(name='widget_id')
Name = String(name='name')
Description = String(name='description', required=False)
Deprecated = Bool(name='deprecated', required=False)
Each field’s name parameter determines the key in the output dictionary.
Common Field Parameters¶
Every field type accepts these parameters:
Parameter |
Default |
Description |
|---|---|---|
|
|
Key name in the output dictionary. |
|
|
Whether the field is included in partial generation mode. |
|
|
Whether the field value can be |
|
|
Static value or callable used instead of auto-generation. |
Nested Schemas¶
Schemas can nest arbitrarily to model complex data structures:
from surety import Dictionary, String, Int, Array, Bool
from surety.sdk.fakeable import Fakeable
class Address(Dictionary):
Street = String(name='street', fake_as=Fakeable.StreetAddress)
City = String(name='city')
State = String(name='state')
ZipCode = String(name='zip_code')
class PaymentMethod(Dictionary):
CardType = String(name='card_type', fake_as=Fakeable.CreditCardProvider)
LastFour = String(name='last_four', min_len=4, max_len=4)
IsDefault = Bool(name='is_default')
class Customer(Dictionary):
CustomerId = Int(name='customer_id', min_val=1000, max_val=99999)
Email = String(name='email')
BillingAddress = Address(name='billing_address')
ShippingAddress = Address(name='shipping_address')
PaymentMethods = Array(PaymentMethod, name='payment_methods', min_len=1,
max_len=3)
customer = Customer(is_full=True)
print(customer.value)
# {
# 'customer_id': 48271,
# 'email': 'margaret.jones@example.com',
# 'billing_address': {
# 'street': '742 Evergreen Terrace',
# 'city': 'Springfield',
# 'state': 'IL',
# 'zip_code': '62704'
# },
# 'shipping_address': { ... },
# 'payment_methods': [
# {'card_type': 'Visa', 'last_four': '4242', 'is_default': True}
# ]
# }
Array¶
Wraps a field type to represent a list of elements:
from surety import Array, Dictionary, String, Int
class Tag(Dictionary):
TagId = Int(name='tag_id')
Label = String(name='label', max_len=30)
class Article(Dictionary):
Title = String(name='title')
Tags = Array(Tag, name='tags', min_len=1, max_len=5)
Parameter |
Default |
Description |
|---|---|---|
|
(required) |
The field type (class) for array elements. |
|
|
Minimum number of elements generated. |
|
|
Maximum number of elements generated. |
Arrays support indexing, iteration, length, and appending:
article = Article()
for tag in article.Tags:
print(tag.Label.value)
first_tag = article.Tags[0]
print(len(article.Tags))
Set¶
Works like Array but produces a set of unique elements:
from surety import Set, String
class PermissionSet(Dictionary):
Roles = Set(String, name='roles', min_len=2, max_len=4)
Generation Modes¶
Full vs Partial¶
By default, Dictionary uses is_full=False — only required fields are
generated. Use is_full=True to include optional fields:
# Partial — only required fields
partial = Widget()
print(partial.value)
# {'widget_id': 7312, 'name': 'RxkPmW'}
# Full — all fields including optional
full = Widget(is_full=True)
print(full.value)
# {'widget_id': 5094, 'name': 'aQdLnZ', 'description': 'Lorem ipsum', 'deprecated': False}
Nullable Fields¶
Fields with allow_none=True will not be generated in partial mode. In full
mode, they receive a value:
class Nullable(Dictionary):
Primary = String(name='primary')
Secondary = String(name='secondary', allow_none=True)
Nullable().value # {'primary': '...'}
Nullable(is_full=True).value # {'primary': '...', 'secondary': '...'}
Default Values¶
Provide a static value or a callable:
class Config(Dictionary):
Version = Int(name='version', default=1)
Region = String(name='region', default='us-east-1')
RequestId = String(name='request_id', default=lambda: 'req-0001')
Accessing Values¶
widget = Widget()
widget.value # Dict of generated fields only
widget.full_value # Dict of all fields, including None for ungenerated
Overriding Values¶
Use with_values() to set specific fields while keeping the rest
auto-generated:
customer = Customer().with_values({
'customer_id': 1,
'email': 'test@acme.org'
})
For nested structures, pass nested dictionaries:
customer = Customer().with_values({
'billing_address': {'city': 'Portland', 'state': 'OR'}
})
For array fields, pass a list:
article = Article().with_values({
'tags': [
{'tag_id': 10, 'label': 'python'},
{'tag_id': 20, 'label': 'testing'}
]
})
Regenerating¶
Call generate() to regenerate all field values:
customer = Customer()
first_email = customer.Email.value
customer.generate()
second_email = customer.Email.value
# first_email != second_email
Custom Fields¶
Create custom field types by subclassing Field and implementing
generate_value():
import random
from surety import Field
class HexColor(Field):
def generate_value(self):
return '#' + ''.join(random.choices('0123456789abcdef', k=6))
class VersionString(Field):
def generate_value(self):
return f'{random.randint(0, 9)}.{random.randint(0, 99)}.{random.randint(0, 999)}'
class Theme(Dictionary):
PrimaryColor = HexColor(name='primary_color')
ApiVersion = VersionString(name='api_version')
Defining a Contract¶
A contract binds one or more schemas to communication semantics. Contracts are defined in extension packages that provide execution capabilities.
API Contract¶
An API contract (from surety-api) binds request and response schemas to an
HTTP method and endpoint path:
from surety.api import ApiContract, HttpMethod
from surety import Dictionary, String, Int
# Schemas — define the shape of data
class CreateOrderRequest(Dictionary):
ProductId = Int(name='product_id')
Quantity = Int(name='quantity', min_val=1, max_val=100)
class OrderResponse(Dictionary):
OrderId = Int(name='order_id')
Status = String(name='status', default='pending')
Total = String(name='total')
# Contract — binds schemas to an API endpoint
class CreateOrder(ApiContract):
method = HttpMethod.POST
url = '/api/v2/orders'
req_body = CreateOrderRequest
resp_body = OrderResponse
The schemas (CreateOrderRequest, OrderResponse) define what the data
looks like. The contract (CreateOrder) defines where and how the data is
exchanged — the HTTP method, the URL, and which schemas apply to the request
and response.
Database Contract¶
A database contract (from surety-db) binds a schema to a database table via
a SQLAlchemy model:
from surety.db import DbModel
from surety import String, Int, Bool
class Warehouse(DbModel):
__model__ = WarehouseTable # SQLAlchemy model — table reference
WarehouseId = Int(name='id')
Name = String(name='name')
Capacity = Int(name='capacity', min_val=100, max_val=10000)
Active = Bool(name='active')
Here Warehouse is a contract because it binds the schema fields to a
specific database table (WarehouseTable).
See API Testing (surety-api) and Database Testing (surety-db) for full usage details on API and database contracts.