Skip to content

pythonize accepts any value as the payload of a unit enum variant, which serde_json refuses #120

Open

Description

@Chaptyziok

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions