tortoise-json-diagnostics
A modern, highly customizable Python library for formatting JSON Schema validation errors into clear, human-readable code snippets and nested ExceptionGroup trees.
Key Features
- 🌳 Nested ExceptionGroup Trees: Automatically groups flat
jsonschemavalidation errors into structured hierarchy matching your JSON schema layout. - 🧩 Extensible Handler Pipeline: Allows custom error handlers to intercept, transform, and prune specific validation errors before fallback processing.
- ⚙️ Global Formatter Registry: Easily switch or implement custom location and code snippet formatters (e.g., plain text, rich, ...).
- 🐍 Modern Python Native: Built for modern Python with strict typing
Visual Output
Instead of unreadable raw validation objects, tortoise-json-diagnostics formats error groups like this:
| ExceptionGroup: JSON Validation Error
| File "input.json" (2 sub-exceptions)
+-+---------------- 1 ----------------
| ExceptionGroup: Property 'name'
| File "input.json", line 2, column 11 (1 sub-exception)
+-+---------------- 1 ----------------
| tortoise_json_diagnostics.errors.JsonValidationError: 123 is not of type 'string'
| File "input.json", line 2, column 16
| 1 | {
| 2 | "name": 123,
| ^^^
| 3 | "age": -5
+------------------------------------
+---------------- 2 ----------------
| ExceptionGroup: Property 'age'
| File "input.json", line 3, column 10 (1 sub-exception)
+-+---------------- 1 ----------------
| tortoise_json_diagnostics.errors.JsonValidationError: -5 is less than the minimum of 0
| File "input.json", line 3, column 14
| 1 | {
| 2 | "name": 123,
| 3 | "age": -5
| ^^
| 4 | }
+------------------------------------
Installation
Using uv (recommended):
uv add tortoise-json-diagnostics
Using pip:
pip install tortoise-json-diagnostics
Quick Start
from jsonschema import Draft202012Validator
from tortoise_json_diagnostics import DiagnosticJsonParser
schema = {
"type": "object",
"required": ["name", "age"],
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0}
}
}
validator = Draft202012Validator(schema)
parser = DiagnosticJsonParser(validator)
json_text = """
{
"name": 123,
"age": -5
}
""".strip()
data = parser.parse_text(json_text, path="input.json")
Advanced Usage
Global Formatters
Register custom formatters for file locations or text snippets:
from tortoise_json_diagnostics import LocationFormatter, set_global_location_formatter
from tortoise_json_diagnostics import DefaultSpansFormatter, set_global_spans_formatter
class CompactLocationFormatter(LocationFormatter):
def format(self, file_path, span, /) -> str:
if not file_path:
return ""
if not span:
return str(file_path)
return f"{file_path}:{span.end.line + 1}:{span.end.column + 1}"
set_global_location_formatter(CompactLocationFormatter())
set_global_spans_formatter(DefaultSpansFormatter(lines_before=0, lines_after=0))
| ExceptionGroup: JSON Validation Error
| input.json (2 sub-exceptions)
+-+---------------- 1 ----------------
| ExceptionGroup: Property 'name'
| input.json:2:11 (1 sub-exception)
+-+---------------- 1 ----------------
| tortoise_json_diagnostics.errors.JsonValidationError: 123 is not of type 'string'
| input.json:2:16
| 2 | "name": 123,
| ^^^
+------------------------------------
+---------------- 2 ----------------
| ExceptionGroup: Property 'age'
| input.json:3:10 (1 sub-exception)
+-+---------------- 1 ----------------
| tortoise_json_diagnostics.errors.JsonValidationError: -5 is less than the minimum of 0
| input.json:3:14
| 3 | "age": -5
| ^^
+------------------------------------
Built-in Handlers
The package includes built-in handlers for specific JSON Schema validation cases, such as handling additional properties or required fields:
from jsonschema import Draft202012Validator
from tortoise_json_diagnostics import DiagnosticJsonParser
schema = {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0}
},
"additionalProperties": False
}
validator = Draft202012Validator(schema)
json_text = """
{
"nmae": "foo",
"age": 5,
"unknown_field": false
}
""".strip()
from tortoise_json_diagnostics.handlers import AdditionalPropertiesHandler
parser = DiagnosticJsonParser(validator, handlers=[AdditionalPropertiesHandler()])
data = parser.parse_text(json_text, path="input.json")
| ExceptionGroup: JSON Validation Error
| File "input.json" (2 sub-exceptions)
+-+---------------- 1 ----------------
| tortoise_json_diagnostics.handlers.additional_properties_handler.AdditionalPropertyError: Additional properties are not allowed ('nmae' was unexpected). Did you mean: 'name'?
| File "input.json", line 2, column 11
| 1 | {
| 2 | "nmae": "foo",
| ^^^^^^
| 3 | "age": 5,
+---------------- 2 ----------------
| tortoise_json_diagnostics.handlers.additional_properties_handler.AdditionalPropertyError: Additional properties are not allowed ('unknown_field' was unexpected)
| File "input.json", line 4, column 20
| 2 | "nmae": "foo",
| 3 | "age": 5,
| 4 | "unknown_field": false
| ^^^^^^^^^^^^^^^
| 5 | }
+------------------------------------
Custom Error Handlers
You can intercept specific ValidationErrors before they hit the default handler by implementing IErrorHandler:
from tortoise_json_diagnostics import IErrorHandler, JsonValidationError, TextSpan, ErrorMessageFormatter, get_global_message_formatter, ErrorTarget
class CustomTypeMismatchHandler(IErrorHandler):
def handle(self, validator, validation_errors, source_map, json_text, file_path, /):
handled: list[JsonValidationError] = []
unhandled = []
for error in validation_errors:
if error.validator == "type":
json_path: tuple[str | int, ...] = tuple(error.absolute_path)
span: TextSpan | None = TextSpan.from_json_path(json_path, source_map)
formatter: ErrorMessageFormatter = get_global_message_formatter()
message: str = formatter.format(f"[Type Mismatch] {error.message}", json_text, file_path, span)
target = ErrorTarget(json_path, span.start if span is not None else None)
error = JsonValidationError(message, validator, [error], target)
handled.append(error)
else:
unhandled.append(error)
return handled, unhandled
parser = DiagnosticJsonParser(validator, handlers=[CustomTypeMismatchHandler()])
| ExceptionGroup: JSON Validation Error
| File "input.json" (2 sub-exceptions)
+-+---------------- 1 ----------------
| ExceptionGroup: Property 'name'
| File "input.json", line 2, column 11 (1 sub-exception)
+-+---------------- 1 ----------------
| tortoise_json_diagnostics.errors.JsonValidationError: [Type Mismatch] 123 is not of type 'string'
| File "input.json", line 2, column 16
| 1 | {
| 2 | "name": 123,
| ^^^
| 3 | "age": -5
+------------------------------------
+---------------- 2 ----------------
| ExceptionGroup: Property 'age'
| File "input.json", line 3, column 10 (1 sub-exception)
+-+---------------- 1 ----------------
| tortoise_json_diagnostics.errors.JsonValidationError: -5 is less than the minimum of 0
| File "input.json", line 3, column 14
| 1 | {
| 2 | "name": 123,
| 3 | "age": -5
| ^^
| 4 | }
+------------------------------------
License
Release files for tortoise-json-diagnostics 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| tortoise_json_diagnostics-0.2.0.tar.gz | 7.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| tortoise_json_diagnostics-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 18.6 kB
Release files / tortoise_json_diagnostics-0.2.0.tar.gz
| Download URL | tortoise_json_diagnostics-0.2.0.tar.gz |
|---|---|
| Size | 7.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f74e28b65fb00a3749d5ca271cf0ccfac4bfded4a2ff94e4904e957011f5cd6c
|
|
BLAKE2b-256 checksum How to use checksums |
841b51f198b009b705c74fefd002d15b4e733f12bb2485f3379eb55a3c0c81c1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 5, 2026.
Transparency logRelease files / tortoise_json_diagnostics-0.2.0-py3-none-any.whl
| Download URL | tortoise_json_diagnostics-0.2.0-py3-none-any.whl |
|---|---|
| Size | 11.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f15edaf5be752f27dfcbf907a9846ad438a0d079a70d92144187d930908fe2a7
|
|
BLAKE2b-256 checksum How to use checksums |
c5f6d6e70a7ba80c891b931ee82e4b7d2d3c67016f63318d80cd77710b40e989
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 5, 2026.
Transparency log