Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions bindings/python/src/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// ---------------------------------------------------------------------------

use pyo3::prelude::*;
use pyo3::types::PyDict;

/// Parse an approval system's decision string into a boolean.
///
Expand Down Expand Up @@ -33,14 +34,37 @@ pub(crate) fn wrap_future_as_coroutine<'py>(
wrapper.call1((&future,))
}

/// Try `model_dump()` on a Python object (Pydantic BaseModel → dict).
/// Falls back to the original object reference if not a Pydantic model.
/// Try `model_dump(mode="json")` on a Python object (Pydantic BaseModel → JSON-safe dict).
///
/// Serialization strategy (three-tier):
///
/// 1. `model_dump(mode="json")` — preferred. Pydantic emits only JSON-native Python types
/// (str, int, float, list, dict, bool, None). Fields like `cost_usd: Decimal` that have a
/// `@field_serializer` convert correctly here. Any field type that Pydantic knows how to
/// JSON-encode is handled without extra effort.
///
/// 2. `model_dump()` — fallback for objects whose `model_dump()` does not accept a `mode`
/// kwarg (e.g. hand-rolled fakes, legacy Pydantic v1 models). The caller (`emit()`) then
/// calls `json.dumps()` on the result; if any field is still non-JSON-native, that will
/// surface as a `TypeError` rather than silently returning the raw object.
///
/// 3. Return the original object — if neither `model_dump` variant is callable. The caller
/// attempts `json.dumps()` on the raw object; if it is not serializable, a `TypeError`
/// propagates naturally.
pub(crate) fn try_model_dump<'py>(obj: &Bound<'py, PyAny>) -> Bound<'py, PyAny> {
match obj.call_method0("model_dump") {
Ok(dict) => dict,
Err(e) => {
log::debug!("model_dump() failed (falling back to raw object): {e}");
obj.clone()
let py = obj.py();
// Tier 1: model_dump(mode="json") — JSON-safe output from real Pydantic models.
let kwargs = PyDict::new(py);
if kwargs.set_item("mode", "json").is_ok() {
if let Ok(dict) = obj.call_method("model_dump", (), Some(&kwargs)) {
return dict;
}
}
// Tier 2: bare model_dump() — for objects without a mode kwarg (fakes, Pydantic v1).
if let Ok(dict) = obj.call_method0("model_dump") {
return dict;
}
// Tier 3: raw object — let the caller's json.dumps() surface any TypeError.
log::debug!("model_dump() not available — passing raw object to json serialiser");
obj.clone()
}
Loading