Reference implementation of the Object-Oriented JSON Schema language. Requires Python 3.10+ and no third-party dependencies.
# From PyPI
pip install oojs
# Or editable install from repo root (for development)
pip install -e .Or just copy the python/ directory into your project and import as oojs — it has zero dependencies.
from oojs import Registry, validate
registry = Registry()
schema = registry.load_file("examples/clinical.oojs.json")
instance = {
"_type": "Observation",
"id": "obs-001",
"timestamp": "2026-03-29T10:00:00Z",
"subjectId": "patient-42",
"code": "8480-6",
"value": 120,
}
errors = validate(instance, schema.types["Observation"], schema, registry)
if errors:
for e in errors:
print(e) # /path: [ERROR_CODE] human message
else:
print("valid")Holds loaded schemas and provides O(1) lookup by discriminator value.
registry = Registry()
# Load from file, JSON string, or plain dict
schema = registry.load_file("my.oojs.json")
schema = registry.load_json('{"$oojs":"1.0", ...}')
schema = registry.load_dict({"$oojs": "1.0", ...})
# Lookup
schema = registry.get_schema("https://example.org/schemas/my")
typedef = registry.lookup_by_discriminator_value("Observation")
typedef = registry.resolve_type("Observation", schema)
typedef = registry.resolve_type_in("Observation", schema_id)Multiple schemas can be loaded into one registry; cross-schema imports are resolved automatically.
from oojs import Validator
validator = Validator(registry, fail_fast=False)
# Validate a decoded Python dict
errors = validator.validate(instance, typedef, schema)
# Validate a JSON string
errors = validator.validate_json('{"_type":"Observation",...}', "Observation", schema)from oojs import validate
errors = validate(instance, typedef, schema, registry, fail_fast=False)Each ValidationError carries:
| Field | Type | Description |
|---|---|---|
path |
str |
RFC 6901 JSON Pointer to the failing location |
code |
str |
One of the ErrorCode constants (see below) |
message |
str |
Human-readable description |
for e in errors:
print(e) # "/findings/0: [MISSING_REQUIRED] missing required property 'id'"
print(e.path) # "/findings/0"
print(e.code) # "MISSING_REQUIRED"
print(e.message) # "missing required property 'id'"| Code | Trigger |
|---|---|
MISSING_DISCRIMINATOR |
Discriminator property absent from instance |
INVALID_DISCRIMINATOR_TYPE |
Discriminator value is not a string |
UNKNOWN_TYPE |
Discriminator value maps to no loaded type |
ABSTRACT_TYPE |
Discriminator names an abstract type |
TYPE_MISMATCH |
Concrete type is not a subtype of the expected type |
MISSING_REQUIRED |
A required property is absent |
ADDITIONAL_PROPERTY |
An undeclared property is present in closed-world mode |
STRING_TOO_SHORT |
String length < minLength |
STRING_TOO_LONG |
String length > maxLength |
PATTERN_MISMATCH |
String does not match pattern |
ENUM_MISMATCH |
Value not in enum list |
BELOW_MINIMUM |
Number < minimum |
ABOVE_MAXIMUM |
Number > maximum |
BELOW_EXCLUSIVE_MINIMUM |
Number ≤ exclusiveMinimum |
ABOVE_EXCLUSIVE_MAXIMUM |
Number ≥ exclusiveMaximum |
NOT_MULTIPLE_OF |
Number is not a multiple of multipleOf |
NOT_INTEGER |
Float value has a non-zero fractional part for an integer field |
ARRAY_TOO_SHORT |
Array length < minItems |
ARRAY_TOO_LONG |
Array length > maxItems |
ARRAY_DUPLICATE_ITEMS |
Non-unique items with uniqueItems: true |
Raised by Registry methods when a schema document is structurally invalid.
from oojs import SchemaError
try:
registry.load_file("bad.oojs.json")
except SchemaError as e:
print(e)python -m oojs <command> [options]
python -m oojs validate \
--schema examples/clinical.oojs.json \
--type Observation \
instance.json
# Read instance from stdin
echo '{"_type":"Observation",...}' | python -m oojs validate \
--schema examples/clinical.oojs.json \
--type Observation -
# Stop after first error
python -m oojs validate --schema ... --type ... --fail-fast instance.json
# Pre-load an imported schema
python -m oojs validate --schema main.oojs.json --type Foo \
--import base=base.oojs.json instance.jsonExit codes: 0 = valid, 1 = validation errors, 2 = file/parse errors.
# List all types in a schema
python -m oojs inspect examples/clinical.oojs.json
# Show a single type
python -m oojs inspect examples/clinical.oojs.json --type Observation{
"$oojs": "1.0",
"$id": "https://example.org/schemas/my",
"title": "Optional title",
"discriminator": "_type",
"additionalProperties": false,
"imports": {
"base": "https://example.org/schemas/base"
},
"types": {
"MyBase": {
"abstract": true,
"properties": {
"id": { "type": "string", "minLength": 1 },
"score": { "type": "number", "minimum": 0, "maximum": 100 }
},
"required": ["id"]
},
"MyConcrete": {
"extends": "MyBase",
"discriminatorValue": "concrete",
"properties": {
"tags": { "type": "array", "items": { "type": "string" }, "uniqueItems": true },
"status": { "type": "string", "enum": ["active", "inactive"] },
"ref": { "type": "base.SomeType" }
},
"required": ["status"]
}
}
}| Element | Pattern | Examples |
|---|---|---|
| Type names | ^[A-Z][A-Za-z0-9_]*$ |
Observation, ClinicalEntry |
| Property names | ^[a-z_][A-Za-z0-9_]*$ |
subject_id, icdCode |
| Import aliases | same as property names | base, clinical |
| Constraint | Applies to | Description |
|---|---|---|
minLength / maxLength |
string |
Unicode code-point count bounds |
pattern |
string |
ECMA regex; re.search semantics (anchors not implicit) |
format |
string |
Informational only — not enforced in v1.0 |
enum |
string, integer, number |
Allowed values list |
minimum / maximum |
integer, number |
Inclusive bounds |
exclusiveMinimum / exclusiveMaximum |
integer, number |
Exclusive bounds (mutually exclusive with inclusive counterparts) |
multipleOf |
integer, number |
Must be > 0 |
minItems / maxItems |
array |
Length bounds |
uniqueItems |
array |
JSON-serialization equality |
# From the repository root
pip install pytest
pytest python/tests/The test suite covers all schema-loading rules, all validation phases, all constraint types, polymorphic dispatch, fail-fast mode, and the clinical integration example.
# Install build tools (once)
pip install build twine
# Build sdist + wheel from the repo root
python3 -m build
# Upload to PyPI (requires a PyPI account + token)
python3 -m twine upload dist/oojs-*The package source lives in python/ and is exposed as the oojs namespace via pyproject.toml.
The automated script scripts/publish.sh handles versioning and uploading.