Skip to content

Error Handling

T-string backends catch errors that f-strings silently ignore and block injection by construction.

Validation errors

The full example demonstrates three kinds of validation errors and injection prevention:

"""Validation and error handling: t-string safety vs f-string fragility.

Demonstrates how t-string backends detect errors that f-strings silently
ignore, plus injection prevention analogous to SQL parameterized queries.
"""

from __future__ import annotations

import json

from json_tstring import render_data, render_text


def main() -> None:
    print("=== Error 1: Non-serializable value ===\n")

    class DatabaseConnection:
        def __init__(self, host: str):
            self.host = host

    conn = DatabaseConnection("db.example.com")
    try:
        render_text(t'{{"connection": {conn}}}')
    except Exception as exc:
        print(f"  {type(exc).__name__}: {exc}")
        print("  f-string would silently produce repr() junk.\n")

    print("=== Error 2: Invalid key type ===\n")

    key = 42
    try:
        render_text(t'{{{key}: "value"}}')
    except Exception as exc:
        print(f"  {type(exc).__name__}: {exc}")
        print("  JSON keys must be strings — t-strings enforce this.\n")

    print("=== Error 3: Non-finite float ===\n")

    value = float("inf")
    try:
        render_text(t'{{"metric": {value}}}')
    except Exception as exc:
        print(f"  {type(exc).__name__}: {exc}")
        print("  JSON forbids Infinity/NaN — t-strings reject them.\n")

    print("=== Injection safety: f-string vs t-string ===\n")

    # Malicious input that closes the string and injects a new key
    user_input = 'admin", "role": "superuser'

    # f-string: vulnerable — attacker overrides the role
    fstring_result = f'{{"role": "viewer", "username": "{user_input}"}}'
    parsed = json.loads(fstring_result)
    print(f"  f-string parsed role: {parsed.get('role')}")
    print("  ^^^ Injection succeeded: attacker controls the role!\n")

    # t-string: safe — value is properly escaped
    tstring_text = render_text(
        t'{{"role": "viewer", "username": {user_input}}}'
    )
    parsed = json.loads(tstring_text)
    print(f"  t-string parsed role: {parsed.get('role')}")
    print(f"  t-string parsed username: {parsed.get('username')}")
    print("  ^^^ Injection blocked: value properly escaped.")


if __name__ == "__main__":
    main()

Error hierarchy

All backends share the same error hierarchy from tstring-bindings:

TemplateError
├── TemplateParseError       — template syntax is invalid
├── TemplateSemanticError    — valid syntax but invalid semantics (e.g., wrong key type)
└── UnrepresentableValueError — Python value can't be represented in the target format

Errors carry Diagnostic objects with source spans and expression labels for precise error reporting.

Common errors

Non-serializable values

from json_tstring import render_text

conn = SomeObject()
render_text(t'{{"connection": {conn}}}')
# => UnrepresentableValueError

F-strings would silently produce repr() output — invalid JSON.

Invalid key types

from json_tstring import render_text

key = 42
render_text(t'{{{key}: "value"}}')
# => TemplateSemanticError

JSON keys must be strings. T-strings enforce this at render time.

Non-finite floats

from json_tstring import render_text

value = float("inf")
render_text(t'{{"metric": {value}}}')
# => UnrepresentableValueError

JSON (RFC 8259) forbids Infinity and NaN. This library also rejects them for YAML to keep output portable.

Injection prevention

T-strings prevent injection the same way SQL parameterized queries prevent SQL injection:

import json
from json_tstring import render_text

# Malicious input
user_input = 'admin", "role": "superuser'

# f-string: VULNERABLE — attacker overrides the role
fstring = f'{{"role": "viewer", "username": "{user_input}"}}'
json.loads(fstring).get("role")  # => "superuser" (injected!)

# t-string: SAFE — value is properly escaped
tstring = render_text(t'{{"role": "viewer", "username": {user_input}}}')
json.loads(tstring).get("role")  # => "viewer" (correct)

Values are inserted into the parsed AST, not concatenated into strings, so the attacker cannot break out of the value slot.