Type Coercion in Validation Pipelines
In this tutorial, you'll learn about Type Coercion. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Type coercion converts input values from strings (which arrive from HTTP) to their expected types like numbers, booleans, and dates before validation.
What You'll Learn
By the end of this lesson, you will implement safe type coercion, handle edge cases, and integrate coercion into a validation pipeline.
Why It Matters
HTTP requests deliver all values as strings. Without coercion, "123" fails a numeric check and "true" is not recognized as boolean. Coercion bridges HTTP and application types.
Real-World Use
Express.js"Express" >}}.js body parsers coerce JSON types automatically. Query parameters arrive as strings and must be coerced: ?age=30 needs parseInt before validation.
Type Coercion Flow
flowchart LR
String["30"] --> Parse[Parse Int]
Parse --> Success[30 int]
String["true"] --> Boolean[Parse Boolean]
Boolean --> Success2[True bool]
String["2024-01-01"] --> Date[Parse Date]
Date --> Success3[Date object]
Type Coercion Functions
# type_coercion.py
from datetime import datetime
from typing import Any, Dict, List, Optional, Union
class TypeCoercer:
@staticmethod
def to_int(value: Any) -> Optional[int]:
if isinstance(value, int):
return value
if isinstance(value, float):
return int(value)
if isinstance(value, str):
try:
return int(value.strip())
except (ValueError, TypeError):
return None
return None
@staticmethod
def to_float(value: Any) -> Optional[float]:
if isinstance(value, (int, float)):
return float(value)
if isinstance(value, str):
try:
return float(value.strip())
except (ValueError, TypeError):
return None
return None
@staticmethod
def to_bool(value: Any) -> Optional[bool]:
if isinstance(value, bool):
return value
if isinstance(value, str):
if value.lower() in ("true", "1", "yes", "on"):
return True
if value.lower() in ("false", "0", "no", "off"):
return False
if isinstance(value, (int, float)):
return value != 0
return None
@staticmethod
def to_date(value: Any, formats: List[str] = None) -> Optional[str]:
if isinstance(value, datetime):
return value.isoformat()
if isinstance(value, str):
fmt_list = formats or ["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%m/%d/%Y"]
for fmt in fmt_list:
try:
return datetime.strptime(value.strip(), fmt).isoformat()
except ValueError:
continue
return None
coercer = TypeCoercer()
tests = [
("42", "int", coercer.to_int("42")),
("3.14", "float", coercer.to_float("3.14")),
("true", "bool", coercer.to_bool("true")),
("no", "bool", coercer.to_bool("no")),
("2024-01-15", "date", coercer.to_date("2024-01-15")),
("invalid", "int", coercer.to_int("invalid")),
]
for value, target, result in tests:
status = f"-> {result}" if result is not None else "-> None (coercion failed)"
print(f"coerce '{value}' to {target}: {status}")
Expected output:
coerce '42' to int: -> 42
coerce '3.14' to float: -> 3.14
coerce 'true' to bool: -> True
coerce 'no' to bool: -> False
coerce '2024-01-15' to date: -> 2024-01-15T00:00:00
coerce 'invalid' to int: -> None (coercion failed)
Schema Coercer
# schema_coercer.py
from typing import Any, Dict, List, Optional, Type, Union
class SchemaCoercer:
def __init__(self):
self.fields: Dict[str, type] = {}
def add(self, name: str, target_type: type):
self.fields[name] = target_type
def coerce(self, data: Dict) -> Dict:
result = dict(data)
errors = []
for field, target in self.fields.items():
if field not in data:
continue
value = data[field]
coerced = self._coerce_value(value, target)
if coerced is None and value is not None:
errors.append(f"Cannot coerce '{field}' to {target.__name__}")
elif coerced is not None:
result[field] = coerced
return {"data": result, "errors": errors}
def _coerce_value(self, value: Any, target: type) -> Any:
if isinstance(value, target):
return value
coercers = {
int: lambda v: int(v) if isinstance(v, (str, float)) else None,
float: lambda v: float(v) if isinstance(v, (str, int)) else None,
bool: self._to_bool,
str: lambda v: str(v) if v is not None else None,
}
coercer = coercers.get(target)
if coercer:
try:
return coercer(value)
except (ValueError, TypeError):
return None
return value
def _to_bool(self, value: Any) -> Optional[bool]:
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.lower() in ("true", "1", "yes")
if isinstance(value, (int, float)):
return value != 0
return None
coercer = SchemaCoercer()
coercer.add("age", int)
coercer.add("price", float)
coercer.add("active", bool)
coercer.add("name", str)
input_data = {"age": "30", "price": "19.99", "active": "true", "name": 123}
result = coercer.coerce(input_data)
print(f"Coerced: {result['data']}")
print(f"Types: {{k: type(v).__name__ for k, v in result['data'].items()}}")
Expected output:
Coerced: {'age': 30, 'price': 19.99, 'active': True, 'name': '123'}
Types: {'age': 'int', 'price': 'float', 'active': 'bool', 'name': 'str'}
Coercion in Pipeline
# pipeline_coercion.py
from typing import Any, Dict, List, Optional, Type
class PipelineCoercer:
def __init__(self):
self.rules: Dict[str, Type] = {}
def expect(self, field: str, coerce_to: Type):
self.rules[field] = coerce_to
def process(self, data: Dict) -> Dict:
result = dict(data)
for field, target in self.rules.items():
if field in result and result[field] is not None:
coerced = self._coerce(result[field], target)
if coerced is not None:
result[field] = coerced
return result
def _coerce(self, value: Any, target: Type) -> Any:
if isinstance(value, target):
return value
try:
if target == bool and isinstance(value, str):
return value.lower() in ("true", "1")
return target(value)
except (ValueError, TypeError):
return None
pipeline = PipelineCoercer()
pipeline.expect("count", int)
pipeline.expect("ratio", float)
pipeline.expect("enabled", bool)
data = {"count": "5", "ratio": "0.75", "enabled": "true", "name": "Alice"}
processed = pipeline.process(data)
for k, v in processed.items():
print(f" {k}: {v!r} ({type(v).__name__})")
Expected output:
count: 5 (int)
ratio: 0.75 (float)
enabled: True (bool)
name: 'Alice' (str)
Common Mistakes
1. Silent Coercion Failure
Coercion that returns None silently can cause NullPointerException later. Log or return errors.
2. Overly Permissive Coercion
Coercing anything to string hides type errors. "30" vs 30 should be treated differently.
3. Coercing After Validation
Coerce first, then validate. If you validate before coercion, type checks fail for all string inputs.
4. Not Handling Locale
"3.14" in US English vs "3,14" in EU. Use locale-aware coercion for user-facing input.
5. Boolean Gotchas
bool("false") is True because the string is non-empty. Always check string content for boolean coercion.
Practice Questions
1. Why is type coercion needed in API validation?
HTTP delivers all values as strings. The application needs typed values for business logic.
2. What order should coercion and validation happen?
Coerce first, validate second. Otherwise type validation fails on string inputs.
3. How do you safely coerce a string to int?
Use int(value.strip()) in a try/except. Return None (or raise) on failure.
4. What is the boolean string problem?
bool("false") returns True because it is a non-empty string. Always check against known true values.
Challenge
Build a coercion layer for a CSV import endpoint that converts string columns to their schema types and reports all coercion failures in one pass.
FAQ
Mini Project: Coercion Pipeline
# coercion_pipeline.py
from typing import Any, Dict, Type
class CoercionPipeline:
def __init__(self):
self.schema: Dict[str, Type] = {}
def field(self, name: str, typ: Type):
self.schema[name] = typ
def run(self, data: Dict) -> Dict:
result = {}
errors = []
for name, typ in self.schema.items():
val = data.get(name)
if val is not None:
try:
result[name] = typ(val) if not isinstance(val, typ) else val
except (ValueError, TypeError):
errors.append(f"{name}: cannot coerce to {typ.__name__}")
return {"data": result, "errors": errors}
p = CoercionPipeline()
p.field("id", int).field("score", float)
print(p.run({"id": "42", "score": "9.5"}))
print(p.run({"id": "abc"}))
Expected output:
{'data': {'id': 42, 'score': 9.5}, 'errors': []}
{'data': {}, 'errors': ['id: cannot coerce to int']}
What's Next
You understand type coercion. Next, learn custom validators, then validation error handling.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro