使用 Serde 将两种类型转换为一种类型
Convert two types into a single type with Serde
我正在编写一个挂钩到发送回 JSON 的 Web 服务的程序。
当某个 属性 不存在时,它会提供一个空对象,其所有字段均为空字符串,而不是排除该值。当 属性 存在时,部分属性为 u64
。我怎样才能让 Serde 处理这种情况?
Rust 结构
#[derive(Clone, Debug, Deserialize)]
struct WebResponse {
foo: Vec<Foo>,
}
#[derive(Clone, Debug, Deserialize)]
struct Foo {
points: Points,
}
#[derive(Clone, Debug, Deserialize)]
struct Points {
x: u64,
y: u64,
name: String,
}
示例JSON
{
"foo":[
{
"points":{
"x":"",
"y":"",
"name":""
}
},
{
"points":{
"x":78,
"y":92,
"name":"bar"
}
}
]
}
Serde 支持一个有趣的 attributes 选择,可用于自定义类型的序列化或反序列化,同时大部分仍使用派生实现。
在您的情况下,您需要能够解码可指定为多种类型之一的字段,并且不需要来自其他字段的信息来决定如何解码有问题的字段。 #[serde(deserialize_with="$path")]
注释非常适合解决您的问题。
我们需要定义一个函数,将空字符串或整数值解码为 u64
。我们可以对两个字段使用相同的函数,因为我们需要相同的行为。此函数将使用自定义 Visitor
来处理字符串和整数。有点长,但是让你体会到Serde为你所做的一切!
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
use serde::Deserializer;
use serde::de::{self, Unexpected};
use std::fmt;
#[derive(Clone, Debug, Deserialize)]
struct WebResponse {
foo: Vec<Foo>,
}
#[derive(Clone, Debug, Deserialize)]
struct Foo {
points: Points,
}
#[derive(Clone, Debug, Deserialize)]
struct Points {
#[serde(deserialize_with = "deserialize_u64_or_empty_string")]
x: u64,
#[serde(deserialize_with = "deserialize_u64_or_empty_string")]
y: u64,
name: String,
}
struct DeserializeU64OrEmptyStringVisitor;
impl<'de> de::Visitor<'de> for DeserializeU64OrEmptyStringVisitor {
type Value = u64;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an integer or a string")
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
if v == "" {
Ok(0)
} else {
Err(E::invalid_value(Unexpected::Str(v), &self))
}
}
}
fn deserialize_u64_or_empty_string<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(DeserializeU64OrEmptyStringVisitor)
}
fn main() {
let value = serde_json::from_str::<WebResponse>(
r#"{
"foo": [
{
"points": {
"x": "",
"y": "",
"name": ""
}
},
{
"points": {
"x": 78,
"y": 92,
"name": "bar"
}
}
]
}"#,
);
println!("{:?}", value);
}
Cargo.toml
:
[dependencies]
serde = "1.0.15"
serde_json = "1.0.4"
serde_derive = "1.0.15"
在str_or_u64
中,我们使用untagged enum来表示字符串或数字。然后我们可以将该字段反序列化为该枚举并将其转换为数字。
我们使用 deserialize_with
注释 Points
中的两个字段,以告诉它使用我们的特殊转换:
use serde::{Deserialize, Deserializer}; // 1.0.124
use serde_json; // 1.0.64
#[derive(Debug, Deserialize)]
struct WebResponse {
foo: Vec<Foo>,
}
#[derive(Debug, Deserialize)]
struct Foo {
points: Points,
}
#[derive(Debug, Deserialize)]
struct Points {
#[serde(deserialize_with = "str_or_u64")]
x: u64,
#[serde(deserialize_with = "str_or_u64")]
y: u64,
name: String,
}
fn str_or_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StrOrU64<'a> {
Str(&'a str),
U64(u64),
}
Ok(match StrOrU64::deserialize(deserializer)? {
StrOrU64::Str(v) => v.parse().unwrap_or(0), // Ignoring parsing errors
StrOrU64::U64(v) => v,
})
}
fn main() {
let input = r#"{
"foo":[
{
"points":{
"x":"",
"y":"",
"name":""
}
},
{
"points":{
"x":78,
"y":92,
"name":"bar"
}
}
]
}"#;
dbg!(serde_json::from_str::<WebResponse>(input));
}
另请参阅:
- How to transform fields during deserialization using Serde?
只是为未来的观众添加一条注释:如果它有帮助,我已经从接受的答案中实施了解决方案并将其发布为板条箱 serde-this-or-that
。
我在 Performance to explain that an approach with a custom Visitor
as suggested, should perform overall much better than a version with an untagged enum 上添加了一个部分,它也有效。
这里是上面接受的解决方案的简化实现(应该有相同的结果):
use serde::Deserialize;
use serde_json::from_str;
use serde_this_or_that::as_u64;
#[derive(Clone, Debug, Deserialize)]
struct WebResponse {
foo: Vec<Foo>,
}
#[derive(Clone, Debug, Deserialize)]
struct Foo {
points: Points,
}
#[derive(Clone, Debug, Deserialize)]
struct Points {
#[serde(deserialize_with = "as_u64")]
x: u64,
#[serde(deserialize_with = "as_u64")]
y: u64,
name: String,
}
fn main() {
let value = from_str::<WebResponse>(
r#"{
"foo": [
{
"points": {
"x": "",
"y": "",
"name": ""
}
},
{
"points": {
"x": 78,
"y": 92,
"name": "bar"
}
}
]
}"#,
);
println!("{:?}", value);
}
Cargo.toml
看起来像:
[dependencies]
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.79"
serde-this-or-that = "0.4"
我正在编写一个挂钩到发送回 JSON 的 Web 服务的程序。
当某个 属性 不存在时,它会提供一个空对象,其所有字段均为空字符串,而不是排除该值。当 属性 存在时,部分属性为 u64
。我怎样才能让 Serde 处理这种情况?
Rust 结构
#[derive(Clone, Debug, Deserialize)]
struct WebResponse {
foo: Vec<Foo>,
}
#[derive(Clone, Debug, Deserialize)]
struct Foo {
points: Points,
}
#[derive(Clone, Debug, Deserialize)]
struct Points {
x: u64,
y: u64,
name: String,
}
示例JSON
{
"foo":[
{
"points":{
"x":"",
"y":"",
"name":""
}
},
{
"points":{
"x":78,
"y":92,
"name":"bar"
}
}
]
}
Serde 支持一个有趣的 attributes 选择,可用于自定义类型的序列化或反序列化,同时大部分仍使用派生实现。
在您的情况下,您需要能够解码可指定为多种类型之一的字段,并且不需要来自其他字段的信息来决定如何解码有问题的字段。 #[serde(deserialize_with="$path")]
注释非常适合解决您的问题。
我们需要定义一个函数,将空字符串或整数值解码为 u64
。我们可以对两个字段使用相同的函数,因为我们需要相同的行为。此函数将使用自定义 Visitor
来处理字符串和整数。有点长,但是让你体会到Serde为你所做的一切!
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
use serde::Deserializer;
use serde::de::{self, Unexpected};
use std::fmt;
#[derive(Clone, Debug, Deserialize)]
struct WebResponse {
foo: Vec<Foo>,
}
#[derive(Clone, Debug, Deserialize)]
struct Foo {
points: Points,
}
#[derive(Clone, Debug, Deserialize)]
struct Points {
#[serde(deserialize_with = "deserialize_u64_or_empty_string")]
x: u64,
#[serde(deserialize_with = "deserialize_u64_or_empty_string")]
y: u64,
name: String,
}
struct DeserializeU64OrEmptyStringVisitor;
impl<'de> de::Visitor<'de> for DeserializeU64OrEmptyStringVisitor {
type Value = u64;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("an integer or a string")
}
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: de::Error,
{
Ok(v)
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
if v == "" {
Ok(0)
} else {
Err(E::invalid_value(Unexpected::Str(v), &self))
}
}
}
fn deserialize_u64_or_empty_string<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(DeserializeU64OrEmptyStringVisitor)
}
fn main() {
let value = serde_json::from_str::<WebResponse>(
r#"{
"foo": [
{
"points": {
"x": "",
"y": "",
"name": ""
}
},
{
"points": {
"x": 78,
"y": 92,
"name": "bar"
}
}
]
}"#,
);
println!("{:?}", value);
}
Cargo.toml
:
[dependencies]
serde = "1.0.15"
serde_json = "1.0.4"
serde_derive = "1.0.15"
在str_or_u64
中,我们使用untagged enum来表示字符串或数字。然后我们可以将该字段反序列化为该枚举并将其转换为数字。
我们使用 deserialize_with
注释 Points
中的两个字段,以告诉它使用我们的特殊转换:
use serde::{Deserialize, Deserializer}; // 1.0.124
use serde_json; // 1.0.64
#[derive(Debug, Deserialize)]
struct WebResponse {
foo: Vec<Foo>,
}
#[derive(Debug, Deserialize)]
struct Foo {
points: Points,
}
#[derive(Debug, Deserialize)]
struct Points {
#[serde(deserialize_with = "str_or_u64")]
x: u64,
#[serde(deserialize_with = "str_or_u64")]
y: u64,
name: String,
}
fn str_or_u64<'de, D>(deserializer: D) -> Result<u64, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum StrOrU64<'a> {
Str(&'a str),
U64(u64),
}
Ok(match StrOrU64::deserialize(deserializer)? {
StrOrU64::Str(v) => v.parse().unwrap_or(0), // Ignoring parsing errors
StrOrU64::U64(v) => v,
})
}
fn main() {
let input = r#"{
"foo":[
{
"points":{
"x":"",
"y":"",
"name":""
}
},
{
"points":{
"x":78,
"y":92,
"name":"bar"
}
}
]
}"#;
dbg!(serde_json::from_str::<WebResponse>(input));
}
另请参阅:
- How to transform fields during deserialization using Serde?
只是为未来的观众添加一条注释:如果它有帮助,我已经从接受的答案中实施了解决方案并将其发布为板条箱 serde-this-or-that
。
我在 Performance to explain that an approach with a custom Visitor
as suggested, should perform overall much better than a version with an untagged enum 上添加了一个部分,它也有效。
这里是上面接受的解决方案的简化实现(应该有相同的结果):
use serde::Deserialize;
use serde_json::from_str;
use serde_this_or_that::as_u64;
#[derive(Clone, Debug, Deserialize)]
struct WebResponse {
foo: Vec<Foo>,
}
#[derive(Clone, Debug, Deserialize)]
struct Foo {
points: Points,
}
#[derive(Clone, Debug, Deserialize)]
struct Points {
#[serde(deserialize_with = "as_u64")]
x: u64,
#[serde(deserialize_with = "as_u64")]
y: u64,
name: String,
}
fn main() {
let value = from_str::<WebResponse>(
r#"{
"foo": [
{
"points": {
"x": "",
"y": "",
"name": ""
}
},
{
"points": {
"x": 78,
"y": 92,
"name": "bar"
}
}
]
}"#,
);
println!("{:?}", value);
}
Cargo.toml
看起来像:
[dependencies]
serde = { version = "1.0.136", features = ["derive"] }
serde_json = "1.0.79"
serde-this-or-that = "0.4"