Repository navigation
Description
Affected version
pythonize 0.29.0 with pyo3 0.29.2 and serde 1.0.229, on CPython 3.14 (Linux x86_64). Later releases not
checked.
Observed
For an externally tagged enum with a unit variant, depythonize accepts the one-key mapping form {"A": <anything>}
and returns E::A, discarding the value: {"A": None}, {"A": "B"}, {"A": [1, 2, 3]} and {"A": True} all decode
to E::A.
PyEnumAccess::unit_variant returns Ok(()) without reading the value held under the variant key (src/de.rs,
lines 602-604 of the 0.29.0 crate).
Expected, independently justified
A unit variant carries no data. In serde's externally tagged representation the only well-formed spellings of a unit
variant are the bare string "A" and the map {"A": null}. serde_json follows that rule: its
VariantAccess::unit_variant deserialises the value as (), so it accepts {"A":null} and refuses {"A":"B"} with
invalid type: string "B", expected unit. depythonize should likewise read the variant's value as unit when the
mapping form is used: accept None, refuse anything else. The same payload would then be accepted or refused
identically by serde_json::from_str and by depythonize.
Minimal reproducer
Cargo.toml:
[dependencies]
pyo3 = { version = "=0.29.2", features = ["auto-initialize"] }
pythonize = "=0.29.0"
serde = { version = "1", features = ["derive"] }
serde_json = "1"src/main.rs:
use pyo3::prelude::*;
use pyo3::types::PyDict;
#[derive(Debug, serde::Deserialize, PartialEq)]
enum E {
A,
B,
}
fn main() {
Python::attach(|py| {
let cases: Vec<(&str, Bound<'_, PyAny>)> = vec![
("None", py.None().into_bound(py)),
("\"B\"", "B".into_pyobject(py).unwrap().into_any()),
("[1, 2, 3]", vec![1, 2, 3].into_pyobject(py).unwrap().into_any()),
("True", true.into_pyobject(py).unwrap().to_owned().into_any()),
];
for (label, value) in cases {
let d = PyDict::new(py);
d.set_item("A", value).unwrap();
let decoded: Result<E, _> = pythonize::depythonize(d.as_any());
println!("depythonize({{\"A\": {label}}}) -> {decoded:?}");
}
for text in [r#"{"A":null}"#, r#"{"A":"B"}"#, r#"{"A":[1,2,3]}"#, r#"{"A":true}"#] {
let json: Result<E, _> = serde_json::from_str(text);
println!("serde_json::from_str({text}) -> {json:?}");
}
});
}Output (serde_json 1.0.151):
depythonize({"A": None}) -> Ok(A)
depythonize({"A": "B"}) -> Ok(A)
depythonize({"A": [1, 2, 3]}) -> Ok(A)
depythonize({"A": True}) -> Ok(A)
serde_json::from_str({"A":null}) -> Ok(A)
serde_json::from_str({"A":"B"}) -> Err(Error("invalid type: string \"B\", expected unit", line: 1, column: 8))
serde_json::from_str({"A":[1,2,3]}) -> Err(Error("invalid type: sequence, expected unit", line: 1, column: 5))
serde_json::from_str({"A":true}) -> Err(Error("invalid type: boolean `true`, expected unit", line: 1, column: 9))
Activity