https://github.com/woolkingx/schema2object
JSON Schema as object definition - dot-access with Draft-07 validation
https://github.com/woolkingx/schema2object
draft-07 json object python schema tree validation
Last synced: 5 months ago
JSON representation
JSON Schema as object definition - dot-access with Draft-07 validation
- Host: GitHub
- URL: https://github.com/woolkingx/schema2object
- Owner: woolkingx
- License: mit
- Created: 2026-02-11T20:13:18.000Z (5 months ago)
- Default Branch: main
- Last Pushed: 2026-02-11T20:26:52.000Z (5 months ago)
- Last Synced: 2026-02-12T03:53:18.742Z (5 months ago)
- Topics: draft-07, json, object, python, schema, tree, validation
- Language: Python
- Size: 23.4 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
Awesome Lists containing this project
README
# schema2object
[](https://www.python.org/downloads/)
[](https://opensource.org/licenses/MIT)
[](https://json-schema.org/specification-links.html#draft-7)
> **JSON Schema as object definition** — Structure maps to attributes, logic maps to methods.
`schema2object` provides a Python dict wrapper with dot-access notation and automatic JSON Schema Draft-07 validation. Define your data structure once as JSON Schema, then access it naturally with type safety enforced at runtime.
## Features
- 🎯 **Dot-access notation** — `obj.user.name` instead of `obj['user']['name']`
- ✅ **Draft-07 validation** — Type checking, constraints, composition keywords
- 🔒 **Type binding** — Schema validation on every assignment
- 📦 **Zero dependencies** — Pure Python 3.9+, stdlib only
- 🔄 **Auto-wrapping** — Nested dicts/lists become ObjectTree instances
- 🎨 **JSON serialization** — Built-in `ObjectTreeEncoder` support
- 🐍 **Pythonic** — Implements `MutableMapping`, pickle, deepcopy
## Installation
```bash
pip install schema2object
```
## Quick Start
```python
from schema2object import ObjectTree
# Define schema
schema = {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'age': {'type': 'integer', 'minimum': 0},
'email': {'type': 'string', 'pattern': r'^[^@]+@[^@]+$'}
},
'required': ['name']
}
# Create object with validation
user = ObjectTree({'name': 'Alice'}, schema=schema)
# Dot-access
print(user.name) # 'Alice'
# Type checking on assignment
user.age = 30 # ✓ OK
user.age = 'thirty' # ✗ Raises TypeError
# Email validation
user.email = 'alice@example.com' # ✓ OK
user.email = 'invalid' # ✗ Raises TypeError (pattern mismatch)
```
## Core Concepts
### Structure vs. Logic
**Schema keywords → attributes** (raw access):
```python
schema = ObjectTree({'oneOf': [...], 'properties': {...}})
schema.oneOf # Get raw list
```
**Schema logic → methods** (computed access):
```python
data = ObjectTree({...}, schema=schema)
branch = data.one_of() # Select matching branch (XOR logic)
```
### Automatic Wrapping
Nested structures become ObjectTree instances automatically:
```python
obj = ObjectTree({
'user': {
'profile': {
'name': 'Alice'
}
}
})
obj.user.profile.name # Full dot-access chain
```
### Default Auto-Fill
Missing fields get defaults from schema:
```python
schema = {
'properties': {
'status': {'type': 'string', 'default': 'pending'},
'priority': {'type': 'integer', 'default': 0}
}
}
task = ObjectTree({}, schema=schema)
print(task.status) # 'pending'
print(task.priority) # 0
```
## Schema Composition
### oneOf (XOR Logic)
Select the unique matching branch:
```python
schema = {
'oneOf': [
{'properties': {'type': {'const': 'user'}, 'name': {'type': 'string'}}},
{'properties': {'type': {'const': 'bot'}, 'id': {'type': 'integer'}}}
]
}
data = ObjectTree({'type': 'user', 'name': 'Alice'}, schema=schema)
branch = data.one_of() # Selects user branch
# Type binding now works on selected branch
branch.name = 'Bob' # ✓ OK
branch.name = 123 # ✗ TypeError
```
### anyOf (OR Logic)
Get all matching branches:
```python
schema = {
'anyOf': [
{'properties': {'x': {'type': 'integer'}}},
{'properties': {'x': {'type': 'number'}}}
]
}
data = ObjectTree({'x': 42}, schema=schema)
branches = data.any_of() # Returns list of ObjectTree instances
```
### allOf (AND Logic)
Merge all sub-schemas:
```python
schema = {
'allOf': [
{'properties': {'name': {'type': 'string'}}},
{'properties': {'age': {'type': 'integer'}}}
]
}
data = ObjectTree({'name': 'Alice', 'age': 30}, schema=schema)
merged = data.all_of() # Merged schema with both constraints
```
### Conditional Logic (if/then/else)
Branch based on conditions:
```python
schema = {
'if': {'properties': {'role': {'const': 'admin'}}},
'then': {'properties': {'level': {'minimum': 5}}},
'else': {'properties': {'level': {'maximum': 4}}}
}
admin = ObjectTree({'role': 'admin', 'level': 10}, schema=schema)
result = admin.if_then() # Uses 'then' branch
```
### Projection (SELECT)
Filter to schema-defined fields:
```python
schema = {
'properties': {
'name': {'type': 'string'},
'age': {'type': 'integer'}
}
}
data = ObjectTree({'name': 'Alice', 'age': 30, 'extra': 'ignored'}, schema=schema)
clean = data.project() # {'name': 'Alice', 'age': 30}
```
## Draft-07 Validation
### Type Validation
```python
schema = {'properties': {'count': {'type': 'integer'}}}
obj = ObjectTree({}, schema=schema)
obj.count = 42 # ✓ OK
obj.count = 3.14 # ✗ TypeError (float is not integer)
obj.count = True # ✗ TypeError (bool is not integer in Draft-07)
```
### Constraints
**Numeric:**
```python
{'type': 'integer', 'minimum': 0, 'maximum': 100, 'multipleOf': 5}
```
**String:**
```python
{'type': 'string', 'minLength': 2, 'maxLength': 50, 'pattern': r'^[A-Z]'}
```
**Array:**
```python
{'type': 'array', 'minItems': 1, 'maxItems': 10, 'uniqueItems': True}
```
**Object:**
```python
{
'type': 'object',
'required': ['name'],
'minProperties': 1,
'maxProperties': 10,
'additionalProperties': False
}
```
### Enum and Const
```python
schema = {
'properties': {
'status': {'enum': ['pending', 'active', 'done']},
'version': {'const': 2}
}
}
obj = ObjectTree({}, schema=schema)
obj.status = 'active' # ✓ OK
obj.status = 'invalid' # ✗ TypeError
obj.version = 2 # ✓ OK
obj.version = 3 # ✗ TypeError
```
## JSON Serialization
```python
import json
from schema2object import ObjectTree, ObjectTreeEncoder
obj = ObjectTree({'user': {'name': 'Alice', 'age': 30}})
# Using custom encoder
json_str = json.dumps(obj, cls=ObjectTreeEncoder)
# Or convert to dict first
json_str = json.dumps(obj.to_dict())
```
## Python Protocols
### MutableMapping
```python
obj = ObjectTree({'a': 1, 'b': 2})
# Dict-like access
obj['c'] = 3
del obj['a']
'b' in obj # True
len(obj) # 2
list(obj.keys()) # ['b', 'c']
list(obj.values()) # [2, 3]
```
### Copy and Deepcopy
```python
import copy
obj = ObjectTree({'nested': {'value': 42}})
shallow = obj.copy()
deep = copy.deepcopy(obj)
```
### Pickle
```python
import pickle
obj = ObjectTree({'data': [1, 2, 3]})
data = pickle.dumps(obj)
restored = pickle.loads(data)
```
### Merge (|= operator)
```python
a = ObjectTree({'x': 1})
b = ObjectTree({'y': 2})
c = a | b # {'x': 1, 'y': 2}
a |= {'z': 3} # In-place merge
```
## Advanced Usage
### Schema Propagation
Child objects inherit sub-schemas:
```python
schema = {
'properties': {
'user': {
'type': 'object',
'properties': {
'name': {'type': 'string'}
}
}
}
}
obj = ObjectTree({'user': {'name': 'Alice'}}, schema=schema)
# Child has sub-schema
obj.user.name = 'Bob' # ✓ Validated
obj.user.name = 123 # ✗ TypeError
```
### Pattern Properties
```python
schema = {
'patternProperties': {
'^S_': {'type': 'string'},
'^I_': {'type': 'integer'}
}
}
obj = ObjectTree({}, schema=schema)
obj['S_name'] = 'Alice' # ✓ OK
obj['I_count'] = 42 # ✓ OK
obj['S_name'] = 123 # ✗ TypeError
```
### Dependencies
```python
schema = {
'dependencies': {
'credit_card': ['billing_address']
}
}
# If credit_card exists, billing_address is required
```
### Array Validation
```python
schema = {
'type': 'array',
'items': {'type': 'integer'},
'contains': {'const': 42} # At least one item must be 42
}
arr = ObjectTree([1, 42, 3], schema=schema)
arr.contains() # True
```
## API Reference
### ObjectTree
**Constructor:**
```python
ObjectTree(data=None, *, schema=None, **kwargs)
```
**Methods:**
- `one_of()` → ObjectTree — Select unique oneOf branch
- `any_of()` → List[ObjectTree] — Get all anyOf matches
- `all_of()` → ObjectTree — Merge allOf schemas
- `not_of(schema=None)` → bool — Check exclusion
- `if_then()` → ObjectTree — Conditional branch
- `project()` → ObjectTree — Filter to schema fields
- `contains(schema=None)` → bool — Array element check
- `to_dict()` → dict — Unwrap to native Python
**Properties:**
- `is_mapping` → bool
- `is_sequence` → bool
### ObjectTreeEncoder
JSON encoder for ObjectTree instances:
```python
json.dumps(obj, cls=ObjectTreeEncoder)
```
## Common Pitfalls
### 1. Use Mapping, Not dict
ObjectTree is NOT a dict subclass:
```python
# ✗ Wrong
if isinstance(data, dict):
...
# ✓ Correct
from collections.abc import Mapping
if isinstance(data, Mapping):
...
```
### 2. Call Methods Before Type Binding
Schema composition requires method call first:
```python
# ✗ Wrong
obj.field = value # Validates against original schema
# ✓ Correct
branch = obj.one_of()
branch.field = value # Validates against selected branch
```
### 3. Circular Imports
Use TYPE_CHECKING for type hints:
```python
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from schema2object import ObjectTree
```
## Development
```bash
# Clone repository
git clone https://github.com/TODO/schema2object.git
cd schema2object
# Run tests
python3 -m pytest tests/test_objecttree.py -v
# Run specific test class
python3 -m pytest tests/test_objecttree.py::TestTypeBinding -v
```
**Note:** Tests must be run from parent directory due to relative imports.
## Requirements
- Python 3.9+
- No external dependencies
## License
MIT License - see [LICENSE](LICENSE) file for details.
## Contributing
Contributions welcome! Please open an issue or submit a pull request.
## Acknowledgments
Built for the [mcp-mindnext-psm](https://github.com/TODO/mcp-mindnext-psm) project as a lightweight, dependency-free alternative to heavy JSON Schema validators.
## Related Projects
- [jsonschema](https://github.com/python-jsonschema/jsonschema) — Full-featured JSON Schema validator
- [pydantic](https://github.com/pydantic/pydantic) — Data validation using Python type annotations
- [marshmallow](https://github.com/marshmallow-code/marshmallow) — Object serialization/deserialization
---
**Why schema2object?**
Unlike other solutions, `schema2object` provides:
- **Dot-access** instead of bracket notation
- **Zero dependencies** for minimal footprint
- **Schema as object** for uniform access patterns
- **Method-based logic** separating structure from behavior
- **Draft-07 compliance** with full composition support