"""Stable JSON serialization helpers for v2 models.""" from __future__ import annotations import json from dataclasses import asdict, is_dataclass from datetime import datetime from pathlib import Path from typing import Any, TypeVar T = TypeVar("T") def _normalize(obj: Any) -> Any: if obj is None or isinstance(obj, (str, int, float, bool)): return obj if isinstance(obj, datetime): return obj.isoformat() if isinstance(obj, Path): return str(obj).replace("\\", "/") if is_dataclass(obj) and not isinstance(obj, type): return {k: _normalize(v) for k, v in asdict(obj).items()} if isinstance(obj, dict): return {str(k): _normalize(v) for k, v in obj.items()} if isinstance(obj, (list, tuple)): return [_normalize(x) for x in obj] return str(obj) def to_dict(obj: Any) -> dict[str, Any]: data = _normalize(obj) if not isinstance(data, dict): raise TypeError(f"expected dataclass/dict, got {type(obj)!r}") return data def from_dict(cls: type[T], data: dict[str, Any]) -> T: """Best-effort dataclass reconstruction (ignores unknown keys).""" if not is_dataclass(cls): raise TypeError(f"{cls!r} is not a dataclass") import dataclasses fields = {f.name for f in dataclasses.fields(cls)} kwargs = {k: v for k, v in (data or {}).items() if k in fields} return cls(**kwargs) # type: ignore[call-arg] def dumps_json(obj: Any, *, indent: int = 2) -> str: return json.dumps( to_dict(obj) if is_dataclass(obj) and not isinstance(obj, type) else _normalize(obj), ensure_ascii=False, indent=indent, sort_keys=False, ) + "\n" def write_model_json(path: Path, obj: Any) -> None: path = Path(path) path.parent.mkdir(parents=True, exist_ok=True) path.write_text(dumps_json(obj), encoding="utf-8") def read_json(path: Path) -> Any: return json.loads(Path(path).read_text(encoding="utf-8"))