I'm trying to manually deserialize a struct that can use the same JSON attribute as different JSON types (e.g. object or string). For example:
[
{
"Name": "a single unit param",
"Units": "m/s"
},
{
"Name": "a multi unit param",
"Units": {
"Metric": {
"Units": "m/s"
},
"Imperial": {
"Units": "ft/s"
}
}
}
]
What I have so far is below. I don't have enough experience in Rust to figure out if what I'm trying to do is possible.
use serde::de::{self, MapAccess, Visitor};
use serde::{Deserialize, Deserializer}; // 1.0.91
use std::fmt;
#[derive(Debug, Deserialize)]
pub struct SingleUnitParam {
name: String,
units: String,
}
#[derive(Debug, Deserialize)]
pub struct UnitInfo {
units: String,
}
#[derive(Debug, Deserialize)]
pub struct MultiUnits {
metric: UnitInfo,
imperial: UnitInfo,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum StrOrUnitsObj<'a> {
Str(&'a str),
UnitsObj(MultiUnits),
}
#[derive(Debug, Deserialize)]
pub struct MultiUnitParam {
name: String,
units: MultiUnits,
}
#[derive(Debug)]
pub enum Param {
Single(SingleUnitParam),
Multiple(MultiUnitParam),
}
impl<'de> Deserialize<'de> for Param {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
enum Field {
Name,
UnitsAsObj,
UnitsAsStr,
};
impl<'de> Deserialize<'de> for Field {
fn deserialize<D>(deserializer: D) -> Result<Field, D::Error>
where
D: Deserializer<'de>,
{
struct FieldVisitor;
impl<'de> Visitor<'de> for FieldVisitor {
type Value = Field;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("`Name` or `Units`")
}
fn visit_str<E>(self, value: &str) -> Result<Field, E>
where
E: de::Error,
{
match value {
"Name" => Ok(Field::Name),
"Units" => Ok({
let val = StrOrUnitsObj::deserialize(deserializer).unwrap();
match val {
StrOrUnitsObj::Str(s) => Field::UnitsAsObj,
StrOrUnitsObj::UnitsObj(obj) => Field::UnitsAsStr,
}
}),
_ => Err(de::Error::unknown_field(value, FIELDS)),
}
}
}
deserializer.deserialize_identifier(FieldVisitor)
}
}
struct ParamVisitor;
impl<'de> Visitor<'de> for ParamVisitor {
type Value = Param;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("enum Param")
}
fn visit_map<V>(self, mut map: V) -> Result<Param, V::Error>
where
V: MapAccess<'de>,
{
let mut name = None;
let mut units_as_string = None;
let mut units_as_object = None;
while let Some(key) = map.next_key()? {
match key {
Field::Name => {
if name.is_some() {
return Err(de::Error::duplicate_field("Name"));
}
name = Some(map.next_value()?);
}
Field::UnitsAsObj => {
if units_as_object.is_some() {
return Err(de::Error::duplicate_field("Units"));
}
units_as_object = Some(map.next_value()?);
}
Field::UnitsAsStr => {
if units_as_string.is_some() {
return Err(de::Error::duplicate_field("Units"));
}
units_as_string = Some(map.next_value()?);
}
}
}
let name = name.ok_or_else(|| de::Error::missing_field("Name"))?;
if let Some(units_as_object) = units_as_object {
Ok(Param::Multiple(MultiUnitParam {
name: name,
units: units_as_object,
}))
} else {
let units_as_string =
units_as_string.ok_or_else(|| de::Error::missing_field("Units"))?;
Ok(Param::Single(SingleUnitParam {
name: name,
units: units_as_string,
}))
}
}
}
const FIELDS: &'static [&'static str] = &["Name", "Units"];
deserializer.deserialize_struct("Param", FIELDS, ParamVisitor)
}
}
fn main() {
let json_raw = r#"[
{ "Name": "a single unit param", "Units": "m/s" },
{ "Name": "a multi unit param", "Units": { "Metric": { "Units": "m/s" }, "Imperial": { "Units": "ft/s" } } }
]"#;
let j: Vec<Param> = serde_json::from_str(&json_raw).unwrap();
match &j[0] {
Param::Single(p) => {
assert_eq!(p.name, "a single unit param");
assert_eq!(p.units, "m/s");
}
Param::Multiple(_p) => panic!("Expected SingleUnitParam, actual MultiUnitParam"),
}
match &j[1] {
Param::Single(_p) => panic!("Expected MultiUnitParam, actual SingleUnitParam"),
Param::Multiple(p) => {
assert_eq!(p.name, "a multi unit param");
assert_eq!(p.units.metric.units, "m/s");
assert_eq!(p.units.imperial.units, "ft/s");
}
}
}
error[E0434]: can't capture dynamic environment in a fn item
--> src/main.rs:74:70
|
74 | let val = StrOrUnitsObj::deserialize(deserializer).unwrap();
| ^^^^^^^^^^^^
|
= help: use the `|| { ... }` closure form instead
Is there a better way I can return a different Field
result for a JSON key based on the JSON value type? Am I on the right track?
fn visit_str<E>(self, value: &str) -> Result<Field, E>
where
E: de::Error,
{
match value {
"Name" => Ok(Field::Name),
"Units" => Ok({
let val = StrOrUnitsObj::deserialize(deserializer).unwrap();
match val {
StrOrUnitsObj::Str(s) => {
Field::UnitsAsObj
},
StrOrUnitsObj::UnitsObj(obj) => {
Field::UnitsAsStr
}
}
}),
_ => Err(de::Error::unknown_field(value, FIELDS)),
}
}
Your problem arises because you are attempting to use a variable from an outer function in an inner function:
fn outer(id: i32) {
fn inner() {
println!("{}", id);
}
}
You simply cannot do this:
error[E0434]: can't capture dynamic environment in a fn item
--> src/lib.rs:3:24
|
3 | println!("{}", id);
| ^^
|
= help: use the `|| { ... }` closure form instead
See also:
You don't need to write any custom deserialization at all; using Serde's attributes is powerful enough:
use serde::Deserialize; // 1.0.91
use serde_json; // 1.0.39
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct Param<'a> {
name: &'a str,
#[serde(borrow)]
units: Units<'a>,
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
enum Units<'a> {
Str(&'a str),
#[serde(borrow)]
Multi(Multi<'a>),
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct Multi<'a> {
#[serde(borrow)]
metric: SingleUnit<'a>,
#[serde(borrow)]
imperial: SingleUnit<'a>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "PascalCase")]
struct SingleUnit<'a> {
units: &'a str,
}
fn main() {
let json_text = r#"[
{
"Name": "a single unit param",
"Units": "m/s"
},
{
"Name": "a multi unit param",
"Units": {
"Metric": {
"Units": "m/s"
},
"Imperial": {
"Units": "ft/s"
}
}
}
]"#;
let x: Vec<Param<'_>> = serde_json::from_str(json_text).expect("Bad schema");
println!("{:?}", x);
}
See also: