Skip to main content

Explain exactly why and where Python objects fail JSON serialization

Project description

jsonwhy

Python's JSON encoder tells you which type failed, but not where that value is inside a large payload. jsonwhy adds the missing path and reports other problems it finds in the same pass.

from datetime import datetime

import jsonwhy

payload = {
    "users": [{"name": "Ada", "joined": datetime(2026, 7, 12)}],
    "tags": {"python", "json"},
}

jsonwhy.dumps(payload)
jsonwhy.JsonWhyError: JSON serialization failed with 2 issues:
1. $.users[0].joined: Object of type datetime is not JSON serializable.
   Fix: Convert it with value.isoformat(); preserve timezone information.
2. $.tags: Object of type set is not JSON serializable.
   Fix: Convert it to a list; sort first if deterministic output matters.

Install

python -m pip install jsonwhy

Python 3.10 or newer is required. The package has no runtime dependencies.

API

dumps() and dump() accept the standard json options. Valid input is passed through to the standard library; diagnosis only runs after serialization fails.

encoded = jsonwhy.dumps({"ok": True}, indent=2)

with open("data.json", "w", encoding="utf-8") as output:
    jsonwhy.dump({"ok": True}, output)

Use explain() when you want structured results without raising an exception:

issues = jsonwhy.explain(payload)

for issue in issues:
    print(issue.path)
    print(issue.json_pointer)
    print(issue.path_segments)
    print(issue.kind)
    print(issue.suggestion)
    print(issue.as_dict())

Each issue includes both a readable JSONPath-like path such as $.users[0].joined and an RFC 6901 json_pointer such as /users/0/joined. A pointer is None when the location contains a Python key that cannot exist in a JSON object. path_segments provides the same location as structured object keys and array indexes:

("users", 0, "joined")

Use inspect() when traversal metadata matters:

report = jsonwhy.inspect(payload, max_nodes=100_000)

print(report.ok)
print(report.nodes_visited)
print(report.truncated)
print(report.truncation_reasons)
print(report.as_dict())

max_nodes is optional and unlimited by default. A node is the root or a value reached through a mapping, sequence, or custom default result.

For large payloads, issues can be consumed as they are found:

for issue in jsonwhy.iter_issues(payload):
    print(issue.path, issue.message)

Stopping iteration stops the inspection. Deep structures are inspected with an explicit traversal stack rather than Python recursion.

Two shorter checks are also available:

jsonwhy.check(payload)                 # bool
jsonwhy.assert_serializable(payload)   # raises JsonWhyError

Value representations are useful during development but may not belong in production logs. They can be removed without calling repr() on the values:

issues = jsonwhy.explain(payload, include_value_repr=False)

jsonwhy.dumps(
    payload,
    diagnostic_include_value_repr=False,
    diagnostic_max_issues=20,
    diagnostic_max_depth=200,
    diagnostic_max_nodes=100_000,
)

The diagnostic options do not change encoded output. Setting include_value_repr=False controls value_repr only; paths, custom suggestion text, and exception messages may still contain application data.

jsonwhy detects unsupported values and keys, circular references, non-finite floats when allow_nan=False, failures in custom default handlers, and overly deep structures.

Custom suggestions

Applications can register advice for their own types:

class CustomerId:
    pass


jsonwhy.register_suggestion(CustomerId, "Convert it with str(value).")

This changes the suggested fix only. jsonwhy does not silently convert the value.

Command line

The CLI checks Python literals, which is useful for values such as sets, bytes, complex numbers, and tuple keys.

jsonwhy "{'payload': b'hello', 'tags': {'a', 'b'}}"
jsonwhy --json "{'payload': b'hello'}"
jsonwhy --path-style pointer "{'payload': b'hello'}"
jsonwhy --max-issues 10 --max-depth 200 "{'payload': b'hello'}"
jsonwhy --max-nodes 100000 "{'payload': b'hello'}"
jsonwhy --json --redact-values "{'payload': b'hello'}"
jsonwhy --json-report "{'payload': b'hello'}"

Exit status 0 means compatible, 1 means issues were found, and 2 means the input or command was invalid.

Notes

  • A failing dump() may have written part of its output before the error.
  • Diagnosis may call a custom default handler or construct a custom encoder again after the original failure. These hooks should be safe to repeat.
  • Diagnostic text can contain representations of application values. Treat it with the same care as logs and tracebacks.
  • Custom repr() implementations can perform work or have side effects before their resulting text is shortened.

Bug reports are welcome on the GitHub issue tracker.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

jsonwhy-0.4.0.tar.gz (17.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

jsonwhy-0.4.0-py3-none-any.whl (15.5 kB view details)

Uploaded Python 3

File details

Details for the file jsonwhy-0.4.0.tar.gz.

File metadata

  • Download URL: jsonwhy-0.4.0.tar.gz
  • Upload date:
  • Size: 17.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for jsonwhy-0.4.0.tar.gz
Algorithm Hash digest
SHA256 d8d8b86847216650d0da9a2bdadfcaded0e5865c169f7d8437c1ab423fdd47cd
MD5 1ee1a306ddcd9d2c70ec7189ca596b26
BLAKE2b-256 ce882f759a9553ce4eb105d4d37083e4fd5f7e2fa36c49e3a7b82f02ba66950c

See more details on using hashes here.

Provenance

The following attestation bundles were made for jsonwhy-0.4.0.tar.gz:

Publisher: publish.yml on Abhay-2004/jsonwhy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file jsonwhy-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: jsonwhy-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 15.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for jsonwhy-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9be98a0eb5e9e30e6944a785546cae2ac52760ff4b36d9a7fa358f899194374b
MD5 ed625c91dca3e64cf403791029e38595
BLAKE2b-256 b8d7478dddb371e09a074c4a8fe1d296050d5fdc6df9162ad71cbb3c6050f02d

See more details on using hashes here.

Provenance

The following attestation bundles were made for jsonwhy-0.4.0-py3-none-any.whl:

Publisher: publish.yml on Abhay-2004/jsonwhy

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page