Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

### Bug Fixes

- Fixed untagged enum deserialisation for serde >= 1.0.220 with better serde content detection ([#582](https://github.com/ron-rs/ron/pull/582))

## [0.11.0] - 2025-08-27

### API Changes
Expand Down
4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "ron"
# Memo: update version in src/lib.rs too (doc link)
version = "0.11.0"
version = "0.11.1"
license = "MIT OR Apache-2.0"
keywords = ["parser", "serde", "serialization"]
authors = [
Expand Down Expand Up @@ -39,6 +39,8 @@ serde = { version = "1.0.181", default-features = false, features = ["alloc"] }
serde_derive = { version = "1.0.181", default-features = false }
unicode-ident = { version = "1.0", default-features = false }
unicode-segmentation = { version = "1.12.0", optional = true, default-features = false }
typeid = { version = "1.0.1", default-features = false }
once_cell = { version = "1.20", default-features = false, features = ["alloc", "race"] }

[dev-dependencies]
serde = { version = "1.0.181", default-features = false, features = ["std", "derive"] }
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ Please file a [new issue](https://github.com/ron-rs/ron/issues/new) if you come

While RON guarantees roundtrips like Rust -> RON -> Rust for Rust types using non-`deserialize_any`-based implementations, RON does not yet make any guarantees about roundtrips through `ron::Value`. For instance, even when RON -> Rust works, RON -> `ron::Value` -> Rust, or RON -> `ron::Value` -> RON -> Rust may not work. We plan on improving `ron::Value` in an upcoming version of RON, though this work is partially blocked on [serde#1183](https://github.com/serde-rs/serde/issues/1183).

[^serde-enum-hack]: Deserialising an internally, adjacently, or un-tagged enum requires detecting `serde`'s internal `serde::__private::de::content::Content` content type so that RON can describe the deserialised data structure in serde's internal JSON-like format. This detection only works for the automatically-derived [`Deserialize`](https://docs.rs/serde/latest/serde/de/trait.Deserialize.html) impls on enums. See [#451](https://github.com/ron-rs/ron/pull/451) for more details.
[^serde-enum-hack]: Deserialising an internally, adjacently, or un-tagged enum requires detecting `serde`'s internal default buffer/content type so that RON can describe the deserialised data structure in serde's internal JSON-like format. A more robust implementation is blocked on [serde#1183](https://github.com/serde-rs/serde/issues/1183).

[^serde-flatten-hack]: Deserialising a flattened struct from a map requires that the struct's [`Visitor::expecting`](https://docs.rs/serde/latest/serde/de/trait.Visitor.html#tymethod.expecting) implementation formats a string starting with `"struct "`. This is the case for automatically-derived [`Deserialize`](https://docs.rs/serde/latest/serde/de/trait.Deserialize.html) impls on structs. See [#455](https://github.com/ron-rs/ron/pull/455) for more details.

Expand Down
148 changes: 135 additions & 13 deletions src/de/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ impl<'de> Deserializer<'de> {
V: Visitor<'de>,
{
// HACK: switch to JSON enum semantics for JSON content
// Robust impl blocked on https://github.com/serde-rs/serde/pull/2420
// Robust impl blocked on https://github.com/serde-rs/serde/issues/1183
let is_serde_content =
is_serde_content::<V::Value>() || is_serde_tag_or_content::<V::Value>();

Expand Down Expand Up @@ -566,7 +566,7 @@ impl<'de, 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
if name == crate::value::raw::RAW_VALUE_TOKEN {
let src_before = self.parser.pre_ws_src();
self.parser.skip_ws()?;
let _ignored = self.deserialize_ignored_any(serde::de::IgnoredAny)?;
let _ignored = self.deserialize_ignored_any(de::IgnoredAny)?;
self.parser.skip_ws()?;
let src_after = self.parser.src();

Expand Down Expand Up @@ -1028,7 +1028,7 @@ impl<'de, 'a> de::MapAccess<'de> for SerdeEnumContent<'a, 'de> {
{
self.ident
.take()
.map(|ident| seed.deserialize(serde::de::value::StrDeserializer::new(ident)))
.map(|ident| seed.deserialize(de::value::StrDeserializer::new(ident)))
.transpose()
}

Expand All @@ -1047,18 +1047,140 @@ impl<'de, 'a> de::MapAccess<'de> for SerdeEnumContent<'a, 'de> {
}
}

// ensure that these are the same as in the 449_tagged_enum test
fn is_serde_content<T>() -> bool {
matches!(
core::any::type_name::<T>(),
"serde::__private::de::content::Content" | "serde::__private::de::content::Content<'_>"
)
#[derive(serde_derive::Deserialize)]
enum A {}
type B = A;

#[derive(serde_derive::Deserialize)]
#[serde(untagged)]
enum UntaggedEnum {
A(A),
B(B),
}

struct TypeIdDeserializer;

impl<'de> de::Deserializer<'de> for TypeIdDeserializer {
type Error = TypeIdError;

fn deserialize_any<V: Visitor<'de>>(self, _visitor: V) -> Result<V::Value, Self::Error> {
Err(TypeIdError(typeid::of::<V::Value>()))
}

serde::forward_to_deserialize_any! {
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
bytes byte_buf option unit unit_struct newtype_struct seq tuple
tuple_struct map struct enum identifier ignored_any
}
}

#[derive(Debug)]
struct TypeIdError(core::any::TypeId);

impl core::fmt::Display for TypeIdError {
fn fmt(&self, _fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
Ok(())
}
}

impl de::Error for TypeIdError {
#[allow(clippy::unreachable)]
fn custom<T: core::fmt::Display>(_msg: T) -> Self {
unreachable!()
}
}

impl de::StdError for TypeIdError {}

fn type_id_of_untagged_enum_default_buffer() -> core::any::TypeId {
static TYPE_ID: once_cell::race::OnceBox<core::any::TypeId> =
once_cell::race::OnceBox::new();

*TYPE_ID.get_or_init(|| match Deserialize::deserialize(TypeIdDeserializer) {
Ok(UntaggedEnum::A(void) | UntaggedEnum::B(void)) => match void {},
Err(TypeIdError(typeid)) => alloc::boxed::Box::new(typeid),
})
}

typeid::of::<T>() == type_id_of_untagged_enum_default_buffer()
}

fn is_serde_tag_or_content<T>() -> bool {
matches!(
core::any::type_name::<T>(),
"serde::__private::de::content::TagOrContent"
| "serde::__private::de::content::TagOrContent<'_>"
)
#[derive(serde_derive::Deserialize)]
enum A {}

#[derive(serde_derive::Deserialize)]
#[serde(tag = "tag")]
enum InternallyTaggedEnum {
A { a: A },
}

struct TypeIdDeserializer;

impl<'de> de::Deserializer<'de> for TypeIdDeserializer {
type Error = TypeIdError;

fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: Visitor<'de>,
{
visitor.visit_map(self)
}

serde::forward_to_deserialize_any! {
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
bytes byte_buf option unit unit_struct newtype_struct seq tuple
tuple_struct map struct enum identifier ignored_any
}
}

impl<'de> de::MapAccess<'de> for TypeIdDeserializer {
type Error = TypeIdError;

fn next_key_seed<K: DeserializeSeed<'de>>(
&mut self,
_seed: K,
) -> Result<Option<K::Value>, Self::Error> {
Err(TypeIdError(typeid::of::<K::Value>()))
}

#[allow(clippy::unreachable)]
fn next_value_seed<V: DeserializeSeed<'de>>(
&mut self,
_seed: V,
) -> Result<V::Value, Self::Error> {
unreachable!()
}
}

#[derive(Debug)]
struct TypeIdError(core::any::TypeId);

impl core::fmt::Display for TypeIdError {
fn fmt(&self, _fmt: &mut core::fmt::Formatter) -> core::fmt::Result {
Ok(())
}
}

impl de::Error for TypeIdError {
#[allow(clippy::unreachable)]
fn custom<T: core::fmt::Display>(_msg: T) -> Self {
unreachable!()
}
}

impl de::StdError for TypeIdError {}

fn type_id_of_internally_tagged_enum_default_tag_or_buffer() -> core::any::TypeId {
static TYPE_ID: once_cell::race::OnceBox<core::any::TypeId> =
once_cell::race::OnceBox::new();

*TYPE_ID.get_or_init(|| match Deserialize::deserialize(TypeIdDeserializer) {
Ok(InternallyTaggedEnum::A { a: void }) => match void {},
Err(TypeIdError(typeid)) => alloc::boxed::Box::new(typeid),
})
}

typeid::of::<T>() == type_id_of_internally_tagged_enum_default_tag_or_buffer()
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
#![warn(clippy::std_instead_of_alloc)]
#![warn(clippy::std_instead_of_core)]
#![doc = include_str!("../README.md")]
#![doc(html_root_url = "https://docs.rs/ron/0.11.0")]
#![doc(html_root_url = "https://docs.rs/ron/0.11.1")]
#![no_std]

#[cfg(feature = "std")]
Expand Down
108 changes: 0 additions & 108 deletions tests/449_tagged_enum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,114 +41,6 @@ enum OuterEnumUntagged {
Sum { field: InnerEnum, value: i32 },
}

#[test]
fn test_serde_content_hack() {
assert!(matches!(
std::any::type_name::<serde::__private::de::Content>(),
"serde::__private::de::content::Content" | "serde::__private::de::content::Content<'_>"
));
}

#[test]
fn test_serde_internally_tagged_hack() {
// ensure that these are the same as in ron::de module
fn is_serde_content<T>() -> bool {
matches!(
core::any::type_name::<T>(),
"serde::__private::de::content::Content" | "serde::__private::de::content::Content<'_>"
)
}

fn is_serde_tag_or_content<T>() -> bool {
matches!(
core::any::type_name::<T>(),
"serde::__private::de::content::TagOrContent"
| "serde::__private::de::content::TagOrContent<'_>"
)
}

struct Deserializer {
tag_key: Option<String>,
tag_value: String,
field_key: Option<String>,
field_value: i32,
}

impl<'de> serde::Deserializer<'de> for Deserializer {
type Error = ron::Error;

fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
where
V: serde::de::Visitor<'de>,
{
visitor.visit_map(self)
}

// GRCOV_EXCL_START
serde::forward_to_deserialize_any! {
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
bytes byte_buf option unit unit_struct newtype_struct seq tuple
tuple_struct map struct enum identifier ignored_any
}
// GRCOV_EXCL_STOP
}

impl<'de> serde::de::MapAccess<'de> for Deserializer {
type Error = ron::Error;

fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
where
K: serde::de::DeserializeSeed<'de>,
{
assert!(is_serde_tag_or_content::<K::Value>());

if let Some(tag_key) = self.tag_key.take() {
return seed
.deserialize(serde::de::value::StringDeserializer::new(tag_key))
.map(Some);
}

if let Some(field_key) = self.field_key.take() {
return seed
.deserialize(serde::de::value::StringDeserializer::new(field_key))
.map(Some);
}

Ok(None)
}

fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
where
V: serde::de::DeserializeSeed<'de>,
{
if self.field_key.is_some() {
assert!(!is_serde_content::<V::Value>());
return seed.deserialize(serde::de::value::StrDeserializer::new(&self.tag_value));
}

assert!(is_serde_content::<V::Value>());

seed.deserialize(serde::de::value::I32Deserializer::new(self.field_value))
}
}

#[derive(PartialEq, Debug, Deserialize)]
#[serde(tag = "tag")]
enum InternallyTagged {
A { hi: i32 },
}

assert_eq!(
InternallyTagged::deserialize(Deserializer {
tag_key: Some(String::from("tag")),
tag_value: String::from("A"),
field_key: Some(String::from("hi")),
field_value: 42,
}),
Ok(InternallyTagged::A { hi: 42 })
);
}

#[test]
fn test_enum_in_enum_roundtrip() {
let outer = OuterEnum::Variant(Container {
Expand Down
26 changes: 26 additions & 0 deletions tests/581_serde_core_content.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "type")]
enum Message {
Request {
id: u32,
resource: String,
operation: String,
},
Response {
id: u32,
value: String,
},
}

#[test]
fn internally_tagged_enum_serde_core_content_detection() {
let value = Message::Response {
id: 60069,
value: "Foobar".into(),
};
let serialized = ron::to_string(&value).unwrap();
let deserialized: Message = ron::from_str(&serialized).unwrap();
assert_eq!(deserialized, value);
}
Loading
Loading