Simplifies defining schema for and managing JSON objects with non-JSON types
Project description
SprelfJSON - JSONModel
JSONModel simplifies the process of working with JSON data in Python by allowing you to define the structure of your
JSON objects using class annotations. It provides robust parsing (from_json()) and dumping (to_json()) capabilities,
handling a variety of data types and supporting nested and polymorphic JSON structures.
Features:
- Define JSON structure using Python class annotations.
- Automatic parsing of JSON data into Python objects.
- Automatic dumping of Python objects into JSON-compatible dictionaries.
- Support for standard Python types (
string,int,float,bool,list,dict, etc.). - Handling of additional types like
datetime,date,time,timedelta,bytes,re.Pattern,Enum,IntEnum,StrEnum, andIntFlag. - Flexibility to add support for additional data types by subclassing
ModelType. - Seamless handling of nested
JSONModelobjects. - Dynamic parsing of
JSONModelsubclasses based on a designated field. - Ability to define alternate parsing and dumping logic.
- Clear error reporting for validation and parsing issues.
Installation
pip install sprelf-json
If you want to include YAML support:
pip install sprelf-json[yaml]
Basic Usage
Define a simple JSON structure:
from SprelfJSON import JSONModel
class User(JSONModel):
name: str
age: int
is_active: bool = True # Field with a default value
Parse JSON data into a User object:
json_data = {"name": "Alice", "age": 30}
user = User.from_json(json_data)
print(user.name) # Output: Alice
print(user.age) # Output: 30
print(user.is_active) # Output: True (default value)
# JSON data can include the default value explicitly
json_data_with_default = {"name": "Bob", "age": 25, "is_active": False}
user_explicit = User.from_json(json_data_with_default)
print(user_explicit.is_active) # Output: False
Dump a User object back into JSON data:
user_to_dump = User(name="Charlie", age=40, is_active=False)
dumped_data = User.to_json()
print(dumped_data) # Output: {'name': 'Charlie', 'age': 40, 'is_active': False}
# Default values are not included by default unless specified in the class
user_with_default = User(name="David", age=35)
dumped_data_default = user_with_default.to_json()
print(dumped_data_default) # Output: {'name': 'David', 'age': 35} (is_active is omitted)
Defining Models
Define a JSON model by creating a class that inherits from JSONModel and using type annotations for the expected fields.
from __future__ import annotations
from typing import Optional, Union
from SprelfJSON import JSONModel, ModelElem
import datetime
class Product(JSONModel):
id: int
name: str
price: float
tags: list[str] # List of strings
attributes: dict[str, str] # Dictionary with string keys and string values
description: Optional[str] = None # Optional field, can be None
created_at: datetime.datetime # Using a complex type
Fields with Default Values
Provide any default values directly in the class definition:
class Settings(JSONModel):
theme: str = "dark"
notifications_enabled: bool = True
For mutable default values (like lists or dictionaries), use ModelElem with default_factory:
class UserProfile(JSONModel):
username: str
favorite_numbers: ModelElem(list[int], default_factory=list) # Use default_factory for mutable defaults
Handling Different Types
This library handles a variety of common datatypes, converting them to and from native JSON types when dumping and parsing.
JSON Native Types
str, int, float, bool, None are parsed and dumped directly.
Complex Types
datetime.datetime, datetime.date, datetime.time, datetime.timedelta, bytes, re.Pattern are automatically
parsed and dumped to/from appropriate JSON representations (e.g., strings for dates/times, base64 for bytes, string for patterns).
import datetime
import re
class Event(JSONModel):
start_time: datetime.datetime
duration: datetime.timedelta
event_id: bytes
pattern: re.Pattern
# Example usage
json_event = {
"start_time": "2023-10-27T14:00:00.000Z",
"duration": 3600000, # timedelta in milliseconds
"event_id": "YWJjMTIz", # base64 encoded bytes
"pattern": "^[A-Z]+$"
}
event = Event.from_json(json_event)
print(type(event.start_time).__name__) # Output: "datetime"
print(type(event.duration).__name__) # Output: "timedelta"
print(type(event.event_id).__name__) # Output: "bytes"
print(type(event.pattern).__name__) # Output: "Pattern"
dumped_event = event.to_json()
print(dumped_event) # Output: The original JSON
Enums
enum.Enum, enum.IntEnum, enum.StrEnum, and enum.IntFlag are supported. Plain Enums are dumped by name, the others by value. They can be parsed as either.
import enum
class Status(enum.Enum):
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
class ErrorCode(enum.IntEnum):
NOT_FOUND = 404
INTERNAL_ERROR = 500
class Flags(enum.IntFlag):
FLAG_A = 1
FLAG_B = 2
FLAG_C = 4
class Task(JSONModel):
status: Status
error_code: Optional[ErrorCode] = None
flags: Flags
# Example usage
json_task = {
"status": "PROCESSING",
"flags": 3 # FLAG_A | FLAG_B
}
task = Task.from_json(json_task)
print(task.status) # Output: Status.PROCESSING
print(task.flags) # Output: Flags.FLAG_A | Flags.FLAG_B
dumped_task = task.to_json()
print(dumped_task) # Output: {'status': 'PROCESSING', 'flags': 3}
Generic Types
list, dict, set, frozenset, tuple, type, Iterable, Iterator, Generator, Sequence, MutableSequence, Set, MutableSet, Mapping, MutableMapping, Collection, Union, and Optional are supported.
The following classes maintain any lazy properties when parsing (deferring validation until iterated): Iterable, Iterator, Generator
from __future__ import annotations
from typing import Union, Optional
from SprelfJSON import JSONModel
class DataContainer(JSONModel):
items: list[int]
settings: dict[str, bool]
value: Union[str, int]
optional_items: Optional[list[float]]
a_type: type[JSONModel]
coords: tuple[float, float]
tags: set[str]
# Example usage
json_data = {
"items": [1, 2, 3],
"settings": {"enabled": True, "visible": False},
"value": "hello",
"optional_items": [1.1, 2.2],
"a_type": "SomeJSONModelType",
"coords": [10.5, 20.1], # JSON array is parsed as tuple
"tags": ["tag1", "tag2", "tag1"] # JSON array is parsed as set
}
container = DataContainer.from_json(json_data)
print(type(container.items).__name__) # Output: "list"
print(type(container.settings).__name__) # Output: "dict"
print(type(container.value).__name__) # Output: "str"
print(type(container.optional_items).__name__) # Output: "list"
print(type(container.a_type).__name__) # Output: "type"
print(type(container.coords).__name__) # Output: "tuple"
print(type(container.tags).__name__) # Output: "set"
dumped_data = container.to_json()
print(dumped_data) # Note: sets and tuples are dumped as JSON arrays (lists)
Nested Models
You can nest JSONModel definitions within other JSONModels.
class Address(JSONModel):
street: str
city: str
zip_code: str
class Order(JSONModel):
order_num: int
class Customer(JSONModel):
customer_id: int
name: str
shipping_address: Address
past_orders: list[Order]
Dynamic Subclass Parsing
JSONModel can automatically determine and instantiate the correct subclass based on a specified field in the JSON data.
Define a base class and subclasses:
class Shape(JSONModel):
# Base class - often abstract, but can have common fields
__name_field__ = "type" # Field to check for subclass name
__name_field_required__ = True # Require the type field
color: str
class Circle(Shape):
radius: float
@classmethod
def model_identity(cls) -> str:
return "circle" # Value in the 'type' field for this subclass
class Square(Shape):
side_length: float
# Use default identity for this class (class's name, 'Square')
# You can then have a model containing a list of shapes
class Drawing(JSONModel):
shapes: list[Shape] # List can contain Circle or Square objects
Parse JSON containing different shape types:
json_drawing = {
"shapes": [
{"type": "circle", "color": "red", "radius": 10.0},
{"type": "Square", "color": "blue", "side_length": 5.0},
{"type": "circle", "color": "green", "radius": 2.5}
]
}
drawing = Drawing.from_json(json_drawing)
for shape in drawing.shapes:
print(f"Shape color: {shape.color}")
if isinstance(shape, Circle):
print(f"Circle radius: {shape.radius}")
elif isinstance(shape, Square):
print(f"Square side length: {shape.side_length}")
# Output -
# Shape color: red
# Circle radius: 10.0
# Shape color: blue
# Square side length: 5.0
# Shape color: green
# Circle radius: 2.5
By default:
- The name field (specified by
__name_field__) is"__name" - The name field is required
- The model identity for a class (the matching value to find in this name field) is the name of the class (ie.
cls.__name__)
See "Extra Options" section for more details.
Support for Additional Types
To extend the supported types, create a new class that is a subclass of ModelType, implementing
the required methods.
class ModelType_PatternExample(ModelType):
# This is used to test whether this model type applies to the given model element.
# The first ModelType to return True here is the one used.
@classmethod
def test_origin(cls, elem: _BaseModelElem, **kwargs) -> bool:
return elem.origin == re.Pattern
# This is used for validating if a value in a particular field meets the criteria for this type
@classmethod
def is_valid(cls, val: Any, elem: _BaseModelElem, **kwargs) -> bool:
return isinstance(val, elem.origin)
# This is used for parsing a value to the desired type; usually is given a JSON value
@classmethod
def parse(cls, val: Any, elem: _BaseModelElem, **kwargs) -> type[SupportedUnion]:
if isinstance(val, re.Pattern):
return val
if isinstance(val, str):
return re.compile(val)
raise ModelElemError(elem, "Woops, can't parse this!")
# This is used to dump the value into a JSON-compatible type
@classmethod
def dump(cls, val: Any, elem: _BaseModelElem, **kwargs) -> JSONType:
definitely_a_pattern_now = elem.parse_value(val, **kwargs)
return definitely_a_pattern_now.pattern # This is a str
Note:
A list of all
ModelTypesubclasses is cached the first time anyJSONModelobject is dumped, parsed, or validated. As long as your subclass is defined before this, it will be automatically included. If you need further manipulation of the allowed types, see_AliasedModelTypesand_ConcreteModelTypesonModelElem
Alternate Parsing and Dumping
Use AlternateModelElem within a ModelElem definition to specify alternative ways to parse incoming data or dump outgoing data.
These objects are defined like ModelElem, but expect to find a different type, and define a function to convert
from that type to the original type that the ModelElem expects.
When parsing or dumping, it will first attempt to operate in its native type. Only if that fails, it will then attempt
doing so with each of the defined alternate definitions. To override this behavior and forcibly use these alternates, you
may provide the use_alternates_only parameter.
from SprelfJSON import JSONModel, ModelElem, AlternateModelElem
class DataItem(JSONModel):
# Can parse an integer from a string
count: ModelElem(int, alternates=[AlternateModelElem(str, int)])
# Can dump a boolean as a string "true" or "false"
is_valid: ModelElem(bool, alternates=[AlternateModelElem(str, lambda s: s.lower() == "true", jsonifier=lambda b: str(b).lower())],
use_alternates_only=True)
# Example usage
json_data = {
"count": "50", # Input is string
"is_valid": "True" # Input is string
}
item = DataItem.from_json(json_data)
print(item.count) # Output: 50 (parsed as int)
print(item.is_valid) # Output: True (parsed as bool)
dumped_data = item.to_json()
print(dumped_data) # Output: {'count': 50, 'is_valid': "true"} # 'count' dumped as int, 'is_valid' dumped as string
Error Handling
JSONModel uses JSONModelError and a subclass ModelElemError to indicate issues during parsing,
validation, or dumping.
from SprelfJSON import JSONModel, JSONModelError
class StrictModel(JSONModel):
required_field: str
int_field: int
# Example of missing required field error
json_missing = {"int_field": 123}
try:
StrictModel.from_json(json_missing)
except JSONModelError as e:
print(f"Caught expected error: {e}") # Output: Caught expected error: Missing required key 'required_field' on 'StrictModel'.
# Example of invalid type error
json_invalid_type = {"required_field": "hello", "int_field": "not an int"}
try:
StrictModel.from_json(json_invalid_type)
except JSONModelError as e:
print(f"Caught expected error: {e}") # Output: Caught expected error: Model error on key 'int_field' of 'StrictModel': Schema mismatch: Expected type '<class 'int'>', but got 'str' instead
# Example of extra field error (by default)
json_extra_field = {"required_field": "hello", "int_field": 123, "extra": "data"}
try:
StrictModel.from_json(json_extra_field)
except JSONModelError as e:
print(f"Caught expected error: {e}") # Output: Caught expected error: The following keys are not found in the model for 'StrictModel': extra
Extra Options
There are some class-level options in JSONModel to define certain types of behavior by the class it's applied to and all subclasses.
__name_field__: str: When parsing, the name of the JSON field that stores the name of theJSONModelobject to dynamically parse. Defaults as"__name"__name_field_required__: bool: When parsing, will reject any JSON objects that do not have the name field defined. Defaults asFalse__include_name_in_json_output__: bool: When dumping, whether to include the name field in the output. Defaults asFalse__allow_null_json_output__: bool: When dumping, whether to allow null JSON values. Defaults asFalse__include_defaults_in_json_output__: bool: When dumping, whether to include fields whose values are equal to the default value. Defaults asFalse.__allow_extra_fields__: bool: When parsing, whether to ignore extra fields that don't belong to the model. IfFalse, then an error is raised if extra fields are found. Defaults asFalse__exclusions__: list[str]: A list of fields that are defined, but should be ignored for the purposes of parsing/dumping.__eval_context__: dict[str, ...]: A map of modules and classes to include when evaluating the annotations (which are read as strings) into actual types.
There are additional class-level options in ModelElem:
__base64_altchars__: tuple[bytes, ...]: A list of 2-character byte strings that define the allowable base64 alternate characters when parsing a string tobytes. The parser will try each one in order until one succeeds. The dumper will always use the first byte string here. By default, is defined as(b"-_", b"+/"), preferring URL-safe altchars.
JSON Annotating and Duck-Typing
JSONDefinitions contains a few helpers for both annotating and for validating JSON data.
For annotating:
from SprelfJSON import JSONType, JSONObject, JSONModel, JSONArray, JSONContainer, JSONValue
def function(arg: JSONObject) -> JSONType:
...
class ExampleModel(JSONModel):
obj: JSONObject
arr: JSONArray
val: JSONValue # any JSON-compatible type other than object or array
container: JSONContainer # array or object
any: JSONType # any JSON-compatible type
For validating using duck-typing classes:
from __future__ import annotations
from SprelfJSON import JSONObjectLike, JSONLike, JSONArrayLike, JSONContainerLike, JSONValueLike
print(isinstance({"a": 1}, JSONObjectLike)) # Output: True
print(isinstance(1, JSONObjectLike)) # Output: False
print(isinstance([1, 2, 3], JSONArrayLike)) # Output: True
print(isinstance(1, JSONArrayLike)) # Output: False
print(isinstance({"a": 1}, JSONContainerLike)) # Output: True
print(isinstance([1, 2, 3], JSONContainerLike)) # Output: True
print(isinstance(1, JSONValueLike)) # Output: True
print(isinstance("hello", JSONValueLike)) # Output: True
print(isinstance(True, JSONValueLike)) # Output: True
print(isinstance(None, JSONValueLike)) # Output: True
print(isinstance([1, 2, 3], JSONValueLike)) # Output: False
print(isinstance(1, JSONLike)) # Output: True
# Or similarly with subclasses...
print(issubclass(int, JSONValueLike)) # Output: True
print(issubclass(dict[str, int], JSONObjectLike)) # Output: True
print(issubclass(dict[int, int], JSONObjectLike)) # Output: False
Ephemeral Values
The Ephemeral class provides a wrapper for any object, allowing its functionality and identity to be mimicked without exposing its wrapped value to the JSON serialization/deserialization process. This means that objects wrapped in Ephemeral are explicitly not parsed from or dumped to JSON. They exist solely in memory, holding values that are manipulated transiently and then forgotten, making their contents irrelevant to JSON-serializability.
This is intended to be useful for values that are created and used at runtime but should never persist in a serialized form (e.g., database connections, temporary calculations, sensitive runtime data).
Key Characteristics:
- Wrapper: Encapsulates any Python object.
- Accessor Exposure: It delegates attribute access (
__getattr__,__setattr__,__delattr__) to the wrapped object, making it behave much like the object it contains. - In-Memory Only:
Ephemeralinstances and their wrapped values are never included when aJSONModelis dumped to JSON, nor are they expected when parsing JSON into aJSONModel. - Non-JSON-Serializable Contents: Because they don't interact with JSON serialization, the objects wrapped by
Ephemeraldo not need to be JSON-serializable themselves.
Example Usage:
from SprelfJSON import JSONModel, Ephemeral
import datetime
class MyTransientObject:
def __init__(self, data):
self.data = data
def process(self):
return f"Processed: {self.data}"
class DataContainer(JSONModel):
name: str
# This field will be ignored during parsing from JSON and dumping to JSON.
runtime_data: Ephemeral[MyTransientObject]
# You can also set a default value for Ephemeral fields.
# Note that the default value must also be wrapped in Ephemeral.
current_timestamp: Ephemeral[datetime.datetime] = Ephemeral(datetime.datetime.now())
# Creating an instance with an Ephemeral field
transient_obj = MyTransientObject("some important runtime info")
container = DataContainer(
name="Report",
runtime_data=Ephemeral(transient_obj)
)
print(container.name) # Output: Report
print(container.runtime_data.data) # Output: some important runtime info
print(container.runtime_data.process()) # Output: Processed: some important runtime info
print(container.current_timestamp.value) # Output: (Current datetime object)
# Dumping to JSON will omit runtime_data and current_timestamp
# even though runtime_data was explicitly provided and current_timestamp has a default.
dumped_json = container.to_json()
print(dumped_json)
# Output: {'name': 'Report'}
# Parsing from JSON:
# Values provided in JSON for Ephemeral fields will be ignored.
json_input_data = {
"name": "Another Report",
"runtime_data": {"data": "this data will be ignored"}, # This field will be ignored during parsing!
}
parsed_container = DataContainer.from_json(json_input_data)
print(parsed_container.name) # Output: Another Report
# When parsing from JSON, if the JSON data does not contain a value for an Ephemeral field,
# the field will be set to None, unless a default is specified.
print(parsed_container.runtime_data) # Output: None
Utility Methods:
Ephemeral.is_ephemeral(obj: Any) -> bool: Checks if an object is an instance ofEphemeralor has the__is_ephemeral__attribute set toTrue.Ephemeral.unwrap(o: Ephemeral[T] | T) -> T: Returns the wrapped value ifois anEphemeralinstance, otherwise returnsoitself.
Known issues
- When subclassing a
JSONModelsubclass that has a default value in a field, IDEs may provide a warning related to "non-default arguments following default arguments". When actually running the code, there is no issue here, so it's safe to ignore or suppress such warnings.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sprelf_json-2025.10.27.0.tar.gz.
File metadata
- Download URL: sprelf_json-2025.10.27.0.tar.gz
- Upload date:
- Size: 31.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
81f216cc5398ed89f4a06344957f8e5432af95ff855cd22ef0fd9dd7baf0526e
|
|
| MD5 |
532c0fd845604e81d6a1d377c1d35f72
|
|
| BLAKE2b-256 |
163bb53eab522e8be408fe9ef6e0f51af4b9163addab990e9331b65d77955a88
|
Provenance
The following attestation bundles were made for sprelf_json-2025.10.27.0.tar.gz:
Publisher:
publish.yml on cgdilley/SprelfJSON
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sprelf_json-2025.10.27.0.tar.gz -
Subject digest:
81f216cc5398ed89f4a06344957f8e5432af95ff855cd22ef0fd9dd7baf0526e - Sigstore transparency entry: 644272754
- Sigstore integration time:
-
Permalink:
cgdilley/SprelfJSON@c107bbe1c6b8f7b19cbc5d1e7b51736a4b9f0396 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/cgdilley
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c107bbe1c6b8f7b19cbc5d1e7b51736a4b9f0396 -
Trigger Event:
push
-
Statement type:
File details
Details for the file sprelf_json-2025.10.27.0-py3-none-any.whl.
File metadata
- Download URL: sprelf_json-2025.10.27.0-py3-none-any.whl
- Upload date:
- Size: 27.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a01b5707ebc03704d8f8088d95ac872970cc7ff50da0db2d3d0a5bdafab80e2e
|
|
| MD5 |
1e0e24824a2edfc6d86a7230abf21839
|
|
| BLAKE2b-256 |
abe6e30e6a5ffd661699e037611defe0cf6840c4d62ba33029f93b58161d54be
|
Provenance
The following attestation bundles were made for sprelf_json-2025.10.27.0-py3-none-any.whl:
Publisher:
publish.yml on cgdilley/SprelfJSON
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sprelf_json-2025.10.27.0-py3-none-any.whl -
Subject digest:
a01b5707ebc03704d8f8088d95ac872970cc7ff50da0db2d3d0a5bdafab80e2e - Sigstore transparency entry: 644272762
- Sigstore integration time:
-
Permalink:
cgdilley/SprelfJSON@c107bbe1c6b8f7b19cbc5d1e7b51736a4b9f0396 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/cgdilley
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@c107bbe1c6b8f7b19cbc5d1e7b51736a4b9f0396 -
Trigger Event:
push
-
Statement type: