How to only allow one field or the other with Serde? - rust

Say I have this struct:
use serde::{Serialize, Deserialize};
#[derive(Deserialize)]
struct MyStruct {
field_1: Option<usize>, // should only have field_1 or field_2
field_2: Option<usize>, // should only have field_1 or field_2
other_field: String,
}
How can I deserialize this but only allow one of these fields to exist?

The suggestions in the comments to use an enum are likely your best bet. You don't need to replace your struct with an enum, instead you'd add a separate enum type to represent this constraint, e.g.:
use serde::{Serialize, Deserialize};
#[derive(Deserialize)]
enum OneOf {
F1(usize), F2(usize)
}
#[derive(Deserialize)]
struct MyStruct {
one_of_field: OneOf,
other_field: String,
}
Now MyStruct's one_of_field can be initialized with either an F1 or an F2.

#dimo414's answer is correct about the enum being necessary, but the code sample will not function in the way the question is described. This is caused by a couple factors related to how enums are deserialized. Mainly, it will not enforce mutual exclusion of the two variants and will silently pick the first variant that matches and ignore extraneous fields. Another issue is the enum will be treated as a separate structure within MyStruct (Ex: {"one_of_field":{"F1":123},"other_field":"abc"} in JSON).
Solution
For anyone wanting an easy solution, here it is. However keep in mind that variants of the mutually exclusive type can not contain #[serde(flatten)] fields (more information in the issue section). To accommodate neither field_1 or field_2, Option<MutuallyExclusive> can be used in MyStruct.
/// Enum containing mutually exclusive fields. Variants names will be used as the
/// names of fields unless annotated with `#[serde(rename = "field_name")]`.
#[derive(Deserialize)]
enum MutuallyExclusive {
field_1(usize),
field_2(usize),
}
#[derive(Deserialize)]
/// `deny_unknown_fields` is required. If not included, it will not error when both
/// `field_1` and `field_2` are both present.
#[serde(deny_unknown_fields)]
struct MyStruct {
/// Flatten makes it so the variants of MutuallyExclusive are seen as fields of
/// this struct. Without it, foo would be treated as a separate struct/object held
/// within this struct.
#[serde(flatten)]
foo: MutuallyExclusive,
other_field: String,
}
The Issue
TL;DR: It should be fine to use deny_unknown_fields with flatten in this way so long as types used in MutuallyExclusive do not use flatten.
If you read the serde documentation, you may notice it warns that using deny_unknown_fields in conjunction with flatten is unsupported. This is problematic as it throws the long-term reliability of the above code into question. As of writing this, serde will not produce any errors or warnings about this configuration and is able to handle it as intended.
The pull request adding this warning cited 3 issues when doing so:
#[serde(flatten)] error behavior is different to normal one
deny_unknown_fields incorrectly fails with flattened untagged enum
Structs with nested flattens cannot be deserialized if deny_unknown_fields is set
To be honest, I don't really care about the first one. I personally feel it is a bit overly pedantic. It simply states that the error message is not exactly identical between a type and a wrapper for that type using flatten for errors triggered by deny_unknown_fields. This should not have any effect on the functionality of the code above.
However, the other two errors are relevant. They both relate to nested flatten types within a deny_unknown_fields type. Technically the second issue uses untagged for the second layer of nesting, but it has the same effect as flatten in this context. The main idea is that deny_unknown_fields is unable to handle more than a single level of nesting without causing issues. The use case is in any way at fault, but the way deny_unknown_fields and flattened are handled makes it difficult to implement a workaround.
Alternative
However, if anyone still feels uncomfortable with using the above code, you can use this version instead. It will be a pain to work with if there are a lot of other fields, but sidesteps the warning in the documentation.
#[derive(Debug, Deserialize)]
#[serde(untagged, deny_unknown_fields)]
enum MyStruct {
WithField1 {
field_1: usize,
other_field: String,
},
WithField2 {
field_2: usize,
other_field: String,
},
}

You can deserialize your struct and then verify that all the invariants your type should uphold. You can implement Deserialize for your type to this while also relying on the derive macro to do the heavy lifting.
use serde::{Deserialize, Deserializer};
#[derive(Debug, Deserialize)]
#[serde(remote = "Self")]
struct MyStruct {
field_1: Option<usize>, // should only have field_1 or field_2
field_2: Option<usize>, // should only have field_1 or field_2
other_field: String,
}
impl<'de> Deserialize<'de> for MyStruct {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use serde::de::Error;
let s = Self::deserialize(deserializer)?;
if s.field_1.is_some() && s.field_2.is_some() {
return Err(D::Error::custom("should only have field_1 or field_2"));
}
Ok(s)
}
}
fn main() -> () {
dbg!(serde_json::from_value::<MyStruct>(serde_json::json!({
"field_1": 123,
"other_field": "abc"
})));
dbg!(serde_json::from_value::<MyStruct>(serde_json::json!({
"field_2": 456,
"other_field": "abc"
})));
dbg!(serde_json::from_value::<MyStruct>(serde_json::json!({
"field_1": 123,
"field_2": 456,
"other_field": "abc"
})));
}
Playground

Related

How to create an arbitrary HashMap to use in rust rocket for a web API

I try to make a web API with rocket to try out the framework. I managed to return paginated results with a special struct that implements serializable.
However, the API I try to build depends on arbitrary values in a special dictionary. The received values may be strings, integers, bools, or other complex objects. The problem now is, that I'm not able to create a struct that contains "any" since Any is not serializable.
The basic idea would be something like this:
#[derive(Debug, Serialize, Deserialize)]
pub struct Foobar<'a> {
pub id: Uuid,
pub data: HashMap<&'a str, ??????>,
}
Even with enums, the problem remains since there is an infinite count of variations. Let's say, I use an enum to determine strings, bools, integers. When the containing type is another type, I need the json representation of that specific type. Basically another map with string -> any.
The current idea would be to use:
#[derive(Debug, Serialize, Deserialize)]
pub struct Foobar {
pub id: Uuid,
pub data: HashMap<String, rocket::serde::json::Value>,
}
But I don't know how the API will fare when there are non json values (e.g. msgpack).
Has somebody accomplished such a feat with rust/rocket?
After several tries with traits and std::any::Any, I ended up with:
pub type DataObject = HashMap<String, DataValue>;
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub enum DataValue {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
Date(String),
Time(String),
DateTime(String),
Reference(DataObject),
ArrayReference(Vec<DataObject>),
}
This is - more or less - the same as serde_json::Value. It helps to further check the values and validate them against a possible type description of the objects.

How to remove a struct's field?

I make a request to a CouchDB database and get a response. Here's the struct for it:
use couch_rs::{types::{document::DocumentId}, CouchDocument, error::CouchResult};
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, CouchDocument, Default, Debug)]
pub struct GuildEntry {
#[serde(skip_serializing_if = "String::is_empty")]
pub _id: DocumentId,
#[serde(skip_serializing_if = "String::is_empty")]
pub _rev: String,
pub embedColor: String
}
I want to remove _id and _rev fields, or at least create a struct instance with all of the fields, except for those two. I can't afford creating a new struct without those fields and transform it from the response, because there's going to be a lot of fields in the future.
What have I tried/looked into so far:
I've looked into utilizing conversion to serde_json Value and then back, but I'll need to declare 2 identical structs, but with one has 2 fields missing.
I've also seen people mentioning composition, but it's kinda ugly and I'd like to avoid using it.
I can't use Option because CouchDocument requires that _id and _rev must be String and not Option<String>.
How can I accomplish this? Is this even possible?
Composition is really the only way. Though you don't have to make two separate structures for each; you can use one generic structure:
extern crate serde;
#[derive(serde::Deserialize)]
struct WithID<T> {
id: String,
#[serde(flatten)]
inner: T,
}
#[derive(serde::Deserialize)]
struct Foo {
bar: String,
baz: String,
}
type FooWithId = WithID<Foo>;
Implementing Deref and DerefMut for WithID with Target = T would make things easier, as you won't have to reference inner as much.

Why does switching from struct to enum breaks API, exactly?

I encountered an interesting change in a public PR.
Initially they had:
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub struct ParseError(ParseErrorKind);
#[derive(Debug, Clone, PartialEq, Eq, Copy)]
enum ParseErrorKind {
OutOfRange // ... omitting other values here to be short
}
ParseError cannot be instantiated by clients because ParseErrorKind is private. They are making that enum public now, which seems ok, but I suggested an alternative: have ParseError be an enum itself, and leverage the type system instead of imitating it with the notion of "kind". They told me that would be an API breakage, and therefore was not ok.
I think I understand why in theory a struct and an enum are different. But I am not sure to understand why it is incompatible in this precise case.
Since the struct ParseError had no mutable field and cannot be instantiated by clients, there was nothing we could do with the type but to assign it and compare it. It seems both struct and enum support that, so client code is unlikely to require a change to compile with a newer version exposing an enum instead of struct. Or did I miss another use we could have with the struct, that would result in requiring a change in client code?
However there might be an ABI incompatibility too. How does Rust handle the struct in practice, knowing that only the library can construct it? Is there any sort of allocation or deallocation mechanism that requires to know precisely what ParseError is made of at buildtime? And does switching from that exact struct to an enum impact that? Or could it be safe in this particular case? And is that relevant to try to maintain the ABI since it is not guaranteed so far?
That's because every struct has fields, and hence this pattern will work for any struct, but will not compile with an enum:
struct Foo {}
fn returns_a_foo() -> Foo {
// anything that may return a Foo
}
if let Foo { .. } = returns_a_foo() {}
For example, this code compiles:
fn main() {
if let String { .. } = String::new() {}
}
Playground.
And while probably not code you'd write on your own, it's still possible to write, and additionally, possible to generate through a macro. Note that this is then, obviously, not compatible with an enum pattern match:
if let Option { .. } = None {
// Compile error.
}
Playground.

Implement convert From trait with additional argument

I have a situation where I have 2 structs
struct A {
pub one: number;
pub two: number;
}
struct AWithThree {
pub one: number;
pub two: number;
pub three: number;
}
And I want to often convert A structs into AWithThree structs after calculating the three field. Note that in my actual code, both these structs have many more fields than shown here.
It becomes tedious though to always manually type each field that I need in the conversion in a function that takes in an A struct and return an AWithThree struct.
AWithThree {
one: a.one,
two: a.two,
three: calculateThree(a),
}
I'd ideally like to be able to just do
AWithThree {
..a,
three: calculateThree(a),
}
This is something that Typescript supports which makes struct mappings really easy since the required fields are automatically checked. But it seems that Rust doesn't support this.
So that's why I turned to implementing the From trait, but the required method of the From trait is only allowed to take in the type we're converting from
fn from(T) -> Self
I instead though wanted to be able to do
fn from(T, additional_arg: S) -> Self
where in my example above, additional_arg would be the value for three that I would then use to constuct the AWithThree struct from the A struct. I don't want to calculate the value of three inside the From since I want the calling function to pass it in.
Is there a better way to do easy struct conversion in Rust?

How can I use serde to serialize a struct to another Rust data structure?

I have a data structure Document, which I would like to serialize other Rust structs to. It is basically a HashMap for the fields internally, however it interacts with a database API, so I will definitely want to convert other types into those Documents.
For example this struct
struct Entry {
id: String,
user: String,
duration: u32,
location: (f64, f64),
}
I already have a conversion to the Document type using the From trait, however this is an extra place I have to modify when the Entry struct changes. The implementation uses a DocumentBuilder and looks like this:
impl From<Entry> for Document {
fn from(entry: Entry) -> Self {
Document::builder()
.name(&entry.id) // set the name of the document
.field("user", entry.user) // add fields ...
.field("duration", entry.duration)
.field("location", entry.location)
.build() // build a Document
}
}
The field method can assign any value which can be converted to a FieldValue to a key. So the signature of field is:
impl DocumentBuilder {
// ...
pub fn field<T: Into<FieldValue>>(mut self, key: &str, value: T) -> Self { ... }
// ...
}
I would like to use serde and its derive feature to automatically serialize the struct and its fields into a Document. How would I go about doing this? I looked at the wiki for Implementing a Serializer but the example shown writes to a string and I would like to know how I can serialize to a data structure using the builder pattern.
The simplest way to do this would be to use serde_json::from_value (applicable even if you're not using JSON, but requires all fields to be valid JSON [e.g. no non-string keys in hashmaps]):
let entry = Entry {
a: 24,
b: 42,
c: "nice".to_string()
};
let v = serde_json::to_value(&entry).unwrap();
let document: Document = serde_json::from_value(v).unwrap();
Caveat: the value type of Document will have to implement Deserialize, and in a way that can deserialize any value into the correct argument. This can be done by using #[serde(untagged)], but may be prone to certain types being wrong, such as u8 being converted to u64.
Full playground example
A more sophisticated method that doesn't involve any unnecessary copies will require you to write a custom (De)serializer, and a good one to look at would be serde_transcode::transcode, which does the inverse of the thing you want - it converts between two data formats.

Resources