serde icon indicating copy to clipboard operation
serde copied to clipboard

Struct with `tag` and `deny_unknown_fields` cannot deserialize

Open schneems opened this issue 2 years ago • 4 comments

Hello, thanks for serde!

Context

I want to prevent accidentally deserializing data from one struct into another when they share fields, as the semantics might be completely different. This is a part of my work here https://github.com/heroku/buildpacks-ruby/pull/246#discussion_r1432008482 where I'm trying to handle different versions of a toml file stored on disk that would map to different structs.

Expected

I would expect that when I use #[serde(tag = "struct_tag", deny_unknown_fields)] that I can serialize a struct to a string, then deserialize that same string back to the original struct.

Actual

This code results in an error

use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
#[serde(tag = "struct_tag")]
#[serde(deny_unknown_fields)]
struct ROFLtagV1 {
    name: String,
}

fn main() {
    let metadata = ROFLtagV1 {
        name: String::from("richard"),
    };

    let toml_string = toml::to_string(&metadata).unwrap();
    assert_eq!(
        "struct_tag = \"ROFLtagV1\"\nname = \"richard\"".trim(),
        toml_string.trim()
    );
   
    toml::from_str::<ROFLtagV1>(&toml_string).unwrap();
}

Playground link: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=d8e02a6d877bc57d24d60dcf93a1df11

Result:

called `Result::unwrap()` on an `Err` value: Error { inner: Error { inner: TomlError { message: "unknown field `struct_tag`, expected `name`", original: Some("struct_tag = \"ROFLtagV1\"\nname = \"richard\"\n"), keys: [], span: Some(0..10) } } }

It serializes as I would expect, but then it gives an error saying that it doesn't know about the struct_tag field.

Addendum

I understand that the tag feature is originally for enums so this might be an unexpected use case. If there's a better way to tell serde that it should preserve the struct name (or some other unique key/value combination) in order to be strict about deserializing then please let me know.

Update:

  • Playground example showing the problem I'm dealing with: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=e26033d3c8c3c34414fe594674f6d053
  • Asking on Stack Overflow https://stackoverflow.com/questions/77700360/prevent-a-serialized-struct-in-rust-from-being-deserialized-into-a-different-one
  • Asking on Mastodon https://ruby.social/@Schneems/111619648760386358

schneems avatar Dec 20 '23 15:12 schneems

I'm currently working around this using IgnoredAny, e.g.:

#[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
#[serde(tag = "struct_tag")]
#[serde(deny_unknown_fields)]
struct ROFLtagV1 {
    name: String,

    #[serde(default, skip_serializing)]
    struct_tag: serde::de::IgnoredAny,
}

It'd be great to have this behaving as expected.

CJKay avatar Jan 27 '25 14:01 CJKay

Thats an interesting idea. Can you link me to a playground link or post a raw rust example you're using? That snippet cannot compile as serde::de::IgnoredAny is not Eq:

$ cargo run
   Compiling lol v0.1.0 (/private/tmp/df2785de1c0822a767b72fe11814d9bd/lol)
error[E0277]: the trait bound `IgnoredAny: Eq` is not satisfied
   --> src/main.rs:10:5
    |
3   | #[derive(Deserialize, Serialize, Debug, Clone, Eq, PartialEq)]
    |                                                -- in this derive macro expansion
...
10  |     struct_tag: serde::de::IgnoredAny,
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Eq` is not implemented for `IgnoredAny`
    |
note: required by a bound in `AssertParamIsEq`
   --> /Users/rschneeman/.rustup/toolchains/stable-aarch64-apple-darwin/lib/rustlib/src/rust/library/core/src/cmp.rs:363:31

I'm unfamiliar with using that type, I tried making a newtype wrapper to see if I could iml a custom Eq on my newtype but abandoned it pretty quickly.

schneems avatar Jan 27 '25 21:01 schneems

Ah, I didn't see the Eq derive there - that probably throws a spanner in the works, then.

CJKay avatar Jan 28 '25 13:01 CJKay

I stumbled upon the same issue as I wanted to make the tag field required. It seems to be ignored when deserializing (!) Or can somebody tell me how to do that?

jaques-sam-tlv avatar Jul 15 '25 12:07 jaques-sam-tlv