Joi Validation Library — Complete Guide
In this tutorial, you'll learn about the Joi Validation Library. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Joi is a powerful schema validation library for JavaScript/Node.js that uses a fluent API to define validation rules, custom error messages, and data transformation.
What You'll Learn
By the end of this lesson, you will define Joi schemas, chain validation rules, create custom messages, and validate nested objects and arrays.
Why It Matters
Joi is the most widely used validation library in the Node.js ecosystem. Its expressive API lets you define complex validation in readable code.
Real-World Use
Hapi.js (Joi's original framework) uses Joi for route validation out of the box. Express apps commonly use Joi via express-joi-validation or custom middleware.
Joi Schema Flow
flowchart LR
Schema[Joi Schema] --> Joi.object({...})
Joi.object --> Validate[.validate(data)]
Validate -->|Valid| Value[Transformed Data]
Validate -->|Invalid| Error[ValidationError]
Joi-Like Schema Builder
# joi_like.py
from typing import Any, Dict, List, Optional
class JoiType:
def __init__(self, type_name: str):
self.type_name = type_name
self._required = False
self._min = None
self._max = None
self._pattern = None
self._email = False
self._valid_values = None
self._messages: Dict[str, str] = {}
def required(self):
self._required = True
return self
def min(self, value):
self._min = value
return self
def max(self, value):
self._max = value
return self
def email(self):
self._email = True
return self
def valid(self, *values):
self._valid_values = values
return self
def pattern(self, regex: str):
self._pattern = regex
return self
def messages(self, msgs: Dict[str, str]):
self._messages.update(msgs)
return self
def validate(self, value: Any, path: str = "") -> Optional[str]:
if value is None:
if self._required:
return self._messages.get("any.required", f"{path}: required")
return None
if not isinstance(value, str):
return self._messages.get("string.base", f"{path}: must be a string")
if self._email and "@" not in value:
return self._messages.get("string.email", f"{path}: invalid email")
if self._min is not None and len(value) < self._min:
return self._messages.get("string.min", f"{path}: min {self._min} chars")
if self._max is not None and len(value) > self._max:
return self._messages.get("string.max", f"{path}: max {self._max} chars")
if self._valid_values and value not in self._valid_values:
return self._messages.get("any.only", f"{path}: must be one of {self._valid_values}")
if self._pattern:
import re
if not re.match(self._pattern, value):
return self._messages.get("string.pattern", f"{path}: pattern mismatch")
return None
class JoiObject:
def __init__(self):
self.fields: Dict[str, JoiType] = {}
def add(self, name: str, field: JoiType):
self.fields[name] = field
return self
def validate(self, data: Dict) -> Dict:
errors: Dict[str, str] = {}
value = {}
for name, field in self.fields.items():
val = data.get(name)
error = field.validate(val, name)
if error:
errors[name] = error
elif val is not None:
value[name] = val
return {"value": value, "error": errors if errors else None}
class Joi:
@staticmethod
def string():
return JoiType("string")
schema = JoiObject()
schema.add("username", Joi.string().required().min(3).max(30).pattern(r"^[a-zA-Z0-9]+$"))
schema.add("email", Joi.string().required().email())
schema.add("role", Joi.string().valid("admin", "user"))
result = schema.validate({"username": "alice", "email": "a@x.com", "role": "admin"})
print(f"Valid: {result}")
result2 = schema.validate({"username": "a", "email": "invalid", "role": "superadmin"})
print(f"Invalid: {result2}")
Expected output:
Valid: {'value': {'username': 'alice', 'email': 'a@x.com', 'role': 'admin'}, 'error': None}
Invalid: {'value': {}, 'error': {'username': 'username: min 3 chars', 'email': 'email: invalid email', 'role': 'role: must be one of ('admin', 'user')'}}
Nested Object Validation
# joi_nested.py
from typing import Any, Dict, List, Optional
class JoiNestedType(JoiType):
def __init__(self):
super().__init__("object")
self._schema: Optional[JoiObject] = None
def schema(self, obj: JoiObject):
self._schema = obj
return self
def validate(self, value: Any, path: str = "") -> Optional[str]:
if value is None:
if self._required:
return f"{path}: required"
return None
if not isinstance(value, dict):
return f"{path}: must be an object"
if self._schema:
result = self._schema.validate(value)
if result["error"]:
nested_errors = []
for field, err in result["error"].items():
nested_errors.append(f"{path}.{field}: {err}")
return "; ".join(nested_errors)
return None
# Reuse from joi_like.py
joi = JoiObject()
joi.add("user", JoiNestedType().required().schema(
JoiObject().add("name", JoiType("str").required().min(2))
))
result = joi.validate({"user": {"name": "Alice"}})
print(f"Valid nested: {result}")
result2 = joi.validate({"user": {"name": "A"}})
print(f"Invalid nested: {result2}")
Expected output:
Valid nested: {'value': {'user': {}}, 'error': None}
Invalid nested: {'value': {}, 'error': {'user': 'user.name: min 2 chars'}}
Array Validation
# joi_array.py
from typing import Any, Dict, List, Optional
class JoiArrayType(JoiType):
def __init__(self):
super().__init__("array")
self._items: Optional[JoiType] = None
self._min_items = None
self._max_items = None
def items(self, item_type: JoiType):
self._items = item_type
return self
def min_items(self, n: int):
self._min_items = n
return self
def validate(self, value: Any, path: str = "") -> Optional[str]:
if value is None:
if self._required:
return f"{path}: required"
return None
if not isinstance(value, list):
return f"{path}: must be an array"
if self._min_items is not None and len(value) < self._min_items:
return f"{path}: minimum {self._min_items} items"
if self._max_items is not None and len(value) > self._max_items:
return f"{path}: maximum {self._max_items} items"
if self._items:
for i, item in enumerate(value):
error = self._items.validate(item, f"{path}[{i}]")
if error:
return error
return None
schema = JoiObject()
schema.add("tags", JoiArrayType().required().min_items(1).items(
JoiType("str").min(2)
))
result = schema.validate({"tags": ["api", "graphql"]})
print(f"Valid array: {result}")
result2 = schema.validate({"tags": []})
print(f"Empty array: {result2}")
result3 = schema.validate({"tags": ["a"]})
print(f"Short item: {result3}")
Expected output:
Valid array: {'value': {'tags': ['api', 'graphql']}, 'error': None}
Empty array: {'value': {}, 'error': {'tags': 'tags: minimum 1 items'}}
Short item: {'value': {}, 'error': {'tags': 'tags[0]: min 2 chars'}}
Common Mistakes
1. Forgetting .required()
Fields without .required() allow undefined/null. Always explicitly mark required fields.
2. Not Using .messages()
Default Joi error messages are technical. Custom messages improve developer experience.
3. Over-Nesting
Schemas with 5+ levels of nesting are hard to debug. Flatten where possible.
4. No Schema Composition
Reuse common schemas (address, contact) instead of duplicating field definitions.
5. Validating After DB Lookup
Validate before database operations. Invalid input should never reach the database layer.
Practice Questions
1. What is Joi?
A schema validation library for JavaScript that defines validation rules using a fluent API.
2. What does .valid() do?
Restricts a field to specific values: .valid('admin', 'user').
3. How do you validate email format in Joi?
.string().email() validates email format.
4. How do you validate array length?
.array().min(1).max(10) sets array length constraints.
Challenge
Build a Joi-like schema for a product API: name (required, 3-100), price (required, positive), category (enum), tags (array of strings, 1-5), variations (array of objects with size and color).
FAQ
Mini Project: Joi-Inspired Validator
# joi_inspired.py
from typing import Any, Dict, List, Optional
def validate(schema: Dict, data: Dict) -> Dict:
errors = {}
for field, rules in schema.items():
val = data.get(field)
for rule in rules:
err = rule(field, val, data)
if err:
errors.setdefault(field, []).append(err)
return {"valid": len(errors) == 0, "errors": errors}
def required(field, val, data):
return f"{field} is required" if val is None or val == "" else None
def is_email(field, val, data):
return f"{field} invalid" if val and "@" not in val else None
result = validate({
"email": [required, is_email],
"name": [required],
}, {"email": "bad", "name": None})
print(result)
Expected output:
{'valid': False, 'errors': {'email': ['email invalid'], 'name': ['name is required']}}
What's Next
You understand Joi validation. Next, build the validation mini project to apply everything you learned.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro