Serializing Custom Classes#
nqxpack handles plain Python types, NumPy arrays, and many JAX/Flax/NetKet objects out of the box. This page explains what to do when you have a class that the library does not know about yet — either your own or a third-party one.
How objects are reconstructed#
Every object is stored in object.json as a dict with a _target_ key containing the
fully-qualified class path (e.g. "mylib.models.MyModel"). On load, nqxpack imports that
path and calls the class (or a registered factory) with the remaining fields as keyword
arguments.
Because reconstruction goes through a normal Python import, objects defined inside
__main__ (e.g. in a script or notebook) cannot be loaded back — the class must be
importable from a proper module.
Mechanisms at a glance#
Mechanism |
Registration needed |
Best for |
|---|---|---|
|
None |
Your own simple data-holding classes |
|
None |
Your own classes with non-trivial construction |
|
Yes |
Third-party classes whose attrs map directly to constructor args |
|
Yes |
Full control: non-trivial construction, binary data |
|
Yes |
Backward compat: renamed/moved classes in old files |
Option 1 — Dataclasses (zero setup)#
Any standard Python dataclass is serialized automatically. All fields are stored and the constructor is called with them on load.
from dataclasses import dataclass
import nqxpack
@dataclass
class Config:
learning_rate: float
hidden_dim: int
nqxpack.save(Config(learning_rate=1e-3, hidden_dim=64), "config.nk")
cfg = nqxpack.load("config.nk") # Config(learning_rate=0.001, hidden_dim=64)
This works as long as every field is itself serializable (plain types, arrays, other registered types, or nested dataclasses).
Option 2 — __to_json__() method (zero setup)#
Add a __to_json__() instance method to your class. It should return a dict of
keyword arguments that will be passed back to the constructor on load. The _target_
key is added automatically.
class Lattice:
def __init__(self, shape: tuple[int, ...], periodic: bool = True):
self.shape = shape
self.periodic = periodic
def __to_json__(self) -> dict:
return {"shape": self.shape, "periodic": self.periodic}
On load, nqxpack calls Lattice(shape=..., periodic=...).
This is the simplest option for classes you own, and requires no import of nqxpack in your library code.
Option 3 — register_automatic_serialization()#
Use this for third-party classes (where you cannot add __to_json__) when the object’s
constructor arguments are available as attributes.
import nqxpack.registry as reg
class Optimizer:
def __init__(self, lr: float, momentum: float = 0.9):
self.lr = lr
self.momentum = momentum
# Serialize attrs 'lr' and 'momentum'; on load call Optimizer(lr=..., momentum=...)
reg.register_automatic_serialization(Optimizer, "lr", "momentum")
If the attribute name differs from the constructor argument name, use keyword syntax:
# constructor takes 'size'; attribute is called 'N'
reg.register_automatic_serialization(MyHilbert, size="N")
Pass array_to_list=True if any attribute is a small NumPy/JAX array that should be
stored as a JSON list rather than a binary asset.
See nqxpack.registry.register_automatic_serialization() for the full signature.
Option 4 — register_serialization() (full control)#
When construction is non-trivial — e.g. the object holds state that cannot be recovered from a few attributes, or you need to store binary data — provide explicit serializer and deserializer functions.
import nqxpack.registry as reg
class KMeans:
def __init__(self, k: int):
self.k = k
self.centers = None # set after fitting, a numpy array
def serialize_kmeans(model: KMeans) -> dict:
return {"k": model.k, "centers": model.centers}
def deserialize_kmeans(data: dict) -> KMeans:
m = KMeans(data["k"])
m.centers = data["centers"]
return m
reg.register_serialization(KMeans, serialize_kmeans, deserialize_kmeans)
The serializer returns a plain dict; nqxpack recursively serializes every value in it
(so model.centers — a NumPy array — is stored correctly as a binary asset
automatically).
Storing large binary data explicitly#
If you need direct control over binary storage (e.g. neural-network weights in
msgpack), access the nqxpack.registry.AssetManager from the active context:
from nqxpack._src.contextmgr import current_context
def serialize_model(model):
am = current_context().asset_manager
am.write_msgpack("weights.msgpack", model.state_dict())
return {"architecture": model.arch}
def deserialize_model(data: dict):
am = current_context().asset_manager
state = am.read_msgpack("weights.msgpack")
model = MyModel(data["architecture"])
model.load_state_dict(state)
return model
Asset names are automatically namespaced by the object’s position in the JSON tree, so two instances stored at different paths never collide.
Option 5 — register_deserialization() (backward compatibility)#
If a class was renamed or moved between library versions, old .nk files will contain
the old _target_ path. Register a deserialization-only handler for the old path:
from nqxpack._src.lib_v1.custom_types import register_deserialization
def load_old_optimizer(data: dict):
from mylib.optim import NewOptimizer
return NewOptimizer(**data)
register_deserialization(
"mylib.old_module.OldOptimizer",
load_old_optimizer,
min_version=(0, 0, 0), # apply for all file versions
)
The min_version tuple refers to the version of the package that wrote the file
(read from metadata.json). This lets you apply different deserializers depending on
when the file was saved.
Closures#
JAX-style initializers (functions returned by other functions) can be serialized with
nqxpack.registry.register_closure_simple_serialization(). This captures the
enclosing function’s arguments and re-calls it on load. See the API reference for
details.