Build a robust financial transaction validation system using Pydantic's powerful type annotations and custom validators. Learn to validate complex nested data, enforce business rules, handle decimal precision for money, and create type-safe data models perfect for FastAPI applications.
This document explains a Python code example that uses Pydantic to model and validate financial transaction data, provided in pydantic_transaction_example.py.
We'll break down each part of the code and explain its purpose.
from pydantic import BaseModel, Field, field_validator, model_validator from typing import List, Optional from datetime import datetime from decimal import Decimal import re
This section imports necessary modules:
datetime for handling timestampsDecimal for precise financial calculationsre for regular expression matchingclass TransactionBase(BaseModel): transaction_id: str = Field(..., min_length=10, max_length=50) amount: Decimal = Field(..., ge=Decimal('0.01')) timestamp: datetime
TransactionBase is the foundation for all transactions:
transaction_id: A string between 10 and 50 charactersamount: A decimal number greater than or equal to 0.01timestamp: The date and time of the transaction@field_validator('transaction_id') @classmethod def validate_transaction_id(cls, v): if not re.match(r'^[A-Z]{2}\d{8}[A-Z]{2}$', v): raise ValueError('Invalid transaction ID format') return v @field_validator('timestamp') @classmethod def validate_timestamp(cls, v): if v > datetime.now(): raise ValueError('Timestamp cannot be in the future') return v
These custom validators add extra checks:
validate_transaction_id: Ensures the ID follows a specific format (2 letters, 8 digits, 2 letters)validate_timestamp: Makes sure the transaction time is not in the futureclass PaymentTransaction(TransactionBase): payment_method: str recipient: str @model_validator(mode='after') def check_payment_details(self): if self.payment_method == 'CASH' and self.amount > 10000: raise ValueError('Cash payments cannot exceed 10,000') return self
PaymentTransaction extends TransactionBase with:
payment_method: The method of paymentrecipient: Who receives the paymentclass RefundTransaction(TransactionBase): reason: str original_transaction_id: str
RefundTransaction also extends TransactionBase, adding:
reason: Why the refund is being madeoriginal_transaction_id: The ID of the original payment being refundedclass TransactionBatch(BaseModel): batch_id: str transactions: List[TransactionBase] processed_at: Optional[datetime] = None model_config = { 'populate_by_name': True, 'json_encoders': { datetime: lambda v: v.isoformat(), Decimal: lambda v: float(v) } }
TransactionBatch represents a group of transactions:
batch_id: An identifier for the batchtransactions: A list of transactions (can be payments or refunds)processed_at: When the batch was processed (optional)The Config class customizes how Pydantic handles the model:
datetime and Decimal to JSONdef process_transaction_batch(batch_data: dict): try: batch = TransactionBatch(**batch_data) # Simulate processing batch.processed_at = datetime.now() return batch.dict(by_alias=True) except ValueError as e: return f"Validation error: {str(e)}"
This function:
TransactionBatch objectbatch_data = { "batch_id": "BATCH001", "transactions": [ { "transaction_id": "TX12345678AB", "amount": "100.50", "timestamp": "2023-05-15T14:30:00", "payment_method": "CARD", "recipient": "John Doe" }, { "transaction_id": "RX87654321CD", "amount": "50.25", "timestamp": "2023-05-15T15:45:00", "reason": "Product return", "original_transaction_id": "TX12345678AB" } ] } result = process_transaction_batch(batch_data) print(result)
This section demonstrates how to use the models:
process_transaction_batch with this dataTo run this example:
Ensure you have Pydantic installed:
pip install pydantic
Run the script:
python pydantic_transaction_example.py
This example shows how Pydantic can handle complex data structures, perform validations, and prepare data for further processing or storage.