-
-
Notifications
You must be signed in to change notification settings - Fork 779
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Flatten #119
Comments
i think the title should be "flattening of struct member of struct type" or sth... The issue with this kind of feature is the same that struct inheritance has: fields defined in both the parent and the child will collide. I guess it would be possible to implement some kind of |
An example: macro_rules! pagination{
(pub struct $name:ident {$($field:ident : $ty:ty)*}) => (
#[derive(Debug)]
pub struct $name {
offset: i32,
limit: i32,
total: i32,
$($field : $ty),*
}
)
}
#[derive(Debug)]
struct User;
pagination!{
pub struct Users {
users: Vec<User>
}
} I don't think this is a serde-related issue, flattening could be useful for structs in general... There could be a crate for this |
You can do this the other way aroound: struct Flattened<A, B>(pub A, pub B);
#[derive(Serialize, Deserialize, ...)]
struct Pagination {
offset: i32,
limit: i32,
total: i32,
}
type Paginated<T> = Flattened<T, Pagination>; Then manually implement Serialize/Deserialize for |
A use case from http://stackoverflow.com/q/41042767/1103681:
This JSON object: {
"name" : "myobject"
"info" :
{
"counter" : "3"
"foo" : "bar"
}
} The corresponding flat Rust struct should be: #[derive(Deserialize)]
struct Object {
name: String,
#[serde(rename="info.counter")] // wrong syntax here !!
count: i32,
#[serde(rename="info::foo")] // neither this works
foo: String,
} |
Looks like that is the opposite use case actually? That post has hierarchy in the JSON but not the Rust type. Here we have hierarchy in the Rust type but not the JSON. I guess we need to think about both. |
We would also want to support flattening members of enum type. This came up in IRC: struct File {
name: String,
path: String,
#[serde(flatten, rename = "fType")]
f_type: FileType,
}
enum FileType {
#[serde(rename = "file")]
File,
#[serde(rename = "directory")]
Directory {
contents: Vec<File>,
}
} and on the JavaScript side the server expects: { "name": "...", "path": "...", "fType": "directory", "contents": [{ "..." }] } |
A similar use case, where only the enum tag is flattened: struct Event {
index: i32,
time: u64,
line: u32,
#[serde(flatten, tag = "eType")]
data: EventKind,
}
enum EventKind {
State { state: Vec<...>, ... },
Output { ... },
Terminate { ... },
} serializes to this: { "index": ..., "time": ..., "line": ..., "eType": "State", "data": { "state": [ ... ], ... } } |
I hit a similar case of wanting to flatten an enum variant, where I wanted to deserialize any string into this enum, opportunistically as one of the named variants, but falling back to
I ended up just using a |
Another real-world use case for this. Serializing from a nested struct in which each nested struct has a ‘superclass’ |
Here is a use case from "kaoD" that is currently more convenient to handle using rustc-serialize. |
I think we're not short on use-cases. "Unwrapped" has unfortunately different connotation in Rust. |
It would be ideal for
Seems to me there is no way to express this right now. |
I think at this point we have enough use cases to start working on this. I would love a PR for |
@dtolnay do you have any rough thoughts on how to approach this without macros v2? All the approaches I can think of need to know what fields are in the flattened object to generate the outer object's deserialize functions. |
@TedDriggs: For serialization that's true, we'll need some more work like exposing some method that allows us to tell a struct to serialize its fields, but nothing else. Maybe we need to create some helper traits and implement them for everything that uses For deserialization you can forward all unknown fields to the field tagged with |
To make deserializing more concrete. As far as I can see there are two ways of implementing this. The first is that unknown fields should be buffered into an intermediate structure as with untagged enums #739. Then this structure can then be passed as the The other approach is to add an additional trait trait DeserializeFlatten {
type Value;
fn init() -> Self;
fn receive_field(&mut self, key: &str, deserializer: D) -> Result<()>;
fn finalize(self) -> Result<Self::Value>;
} This has the drawback flattened fields must implement this trait to work and while it can be implement as a part of any |
Thanks for the thorough proposals! I would prefer to stick with the first way, like untagged enums. Supporting only self-describing formats is fine because in practice that's where people will care about controlling the representation this way. |
@cmyr I opened #1028 to discuss a custom deserialize approach using existing functionality. Do you think this is a common pattern for JSON APIs? I have never seen it before. If it is not common, a custom Serialize and Deserialize implementation for JsonRpc in your codebase would be the way to go. If it is common, can you find some other APIs that work this way? |
@dtolnay This pattern is used in the JSON-RPC spec, which is used by the Language Server Protocol, and a few other places. The implementation I'm working on (for the xi editor) is derived from JSON-RPC. I have not seen this pattern outside of this context. Other projects dealing with this protocol that I've been able to find tend to be doing custom deserialization, afaict[1]. Anyway: if there's a reasonable way for me to do this with a custom Deserialize (and your post suggests there is!) than that's good enough for me. Thanks! |
A simple |
@dtolnay @cmyr Another example of the "does this field exist" pattern can be seen in the GDAX "full channel" API. For example, the "change" message can either have I think that right now this could be solved with the untagged enum representation, but I'm not sure how it'd compose with this proposed flatten feature (which, btw, I'm very in favor of - it'd probably solve this problem that I have). |
Do people have preferences for how struct A {
a: String,
#[serde(flatten)]
inner: B,
}
enum B {
B1(String),
B2(String),
} Without flatten this is serialized to JSON as: {
"a": "...",
"inner": {
"B1": "..."
}
} So intuitively this would flatten to: {
"a": "...",
"B1": "...",
} But a compact serializer like Bincode could choose not to serialize struct keys, losing information about whether the variant is B1 or B2. Some possibilities:
|
Would there be no way to detect this at compile time? |
@joshlf the type that something serializes as is determined by the code in its Serialize impl, which is executed at runtime. I suppose there could be a marker trait that is implemented for types that pinky promise to serialize in a flattenable way, but bounds in generic code using flatten would be messy and it would be more complicated to use flatten with types that have handwritten Serialize impls. In general marker traits would make Serde feel like it needs more hand-holding rather than just doing the right thing. We already use runtime errors in similar cases such as int in an internally tagged newtype variant. type Opaque = i32;
#[derive(Serialize, Deserialize)]
struct S {
k: String,
}
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")]
enum E {
// okay, serializes as {"type":"A","k":"..."}
A(S),
// runtime error, what does this even mean? 123"type":"B"456
B(Opaque),
} |
Oh, right - I hadn't thought about custom impls. Nevermind! |
A format is free to skip writing struct keys regardless of whether it is "human readable" though so I don't see how that helps (sure, this could be documented, but I'd argue this would be fairly surprising behavior).
Isn't the problem here only that So
seems like the way to go to me. |
Doesn't it already do that for externally-tagged enums? This issue seems orthogonal to flattening. |
Another use case. In MongoDb we want to use the newtype pattern to distinguish between different document ids at compile time, however we can't easily do it with the vanilla newtype pattern because MongoDb doesn't allow us to use an array in the _id field. e.g. we want #[derive(Serialize, Deserialize, Debug)]
pub struct Id(ObjectId) To completely strip the Id and just emit an ObjectId. Right now it emits an array containing the object id. Note that in this particular case there is a work around that isn't too horrible, #[derive(Serialize, Deserialize, Debug)]
#[serde(untagged)]
enum Id {
Id(ObjectId)
} Still it'd be nice to use the idiomatic newtype pattern. |
@estokes I think for your use case a simple workaround is to simply manually implement Serialize and just forward the calls to the inner value. It's probably a good idea to use a macro for that. (The same goes for Deserialize of course.) |
For what it's worth while working on implementing #941 I cam to the same conclusion that others voiced before: they are effectively the same feature and implementing flatten is what should be done. Working on changing this over to fully supporting flatten now. |
I attempted to implement the proposal by @dtolnay from here but I can't make it work because from my understanding of what the adapter has to do it needs to "fake" the return value from I currently generate out something like this for the flattens in serialize ( let mut __serde_state = match _serde::Serializer::serialize_map(__serializer, None) {
::result::Result::Ok(val) => val,
::result::Result::Err(err) => {
return ::result::Result::Err(::convert::From::from(err))
}
};
// ...
match (&self.extra)
.serialize(_serde::private::ser::FlatMapSerializer(&mut __serde_state))
{
::result::Result::Ok(val) => val,
::result::Result::Err(err) => {
return ::result::Result::Err(::convert::From::from(err))
}
};
_serde::ser::SerializeMap::end(__serde_state) The This makes me believe that a change to traits is necessary to make this work unless someone has something I missed. |
From the sketch: let mut map = serializer.serialize_map(None)?;
map.serialize_entry("outer1", &self.outer1)?;
self.inner.serialize(SerializeMapAdapter(&mut map))?;
self.other.serialize(SerializeMapAdapter(&mut map))?;
map.end() I think we just want to ignore the
We can't change the |
Oh indeed. Since we're the only user of that there is no need to propagate the value. |
I have an implementation in #1179 for this now. Would love to get some input on it and suggestions of how to deal with the limitations outlined there. I think it's generally in a mergeable state feature wise but it might need some review from more experienced serde people. To support flattening enums with |
A flatten attribute has been implemented in #1179 and released in Serde 1.0.34. There are some related ideas discussed in this thread; let's file separate issues for those that have not been addressed yet by this initial implementation. |
I am interacting with an API which returns pagination information on most of their requests. Here is an example:
limit
,offset
, andtotal
are present for many of the responses received. Here is the struct I deserialize the JSON response into:The problem that I'm having is that I don't know how I can avoid repeating those same 3 fields in other structs.
I would like to have something similar to this:
Here serde would see that no field
pagination
was present but the type of the struct field isPagination
which contains fields which are present in the response.I guess this could be solved with some kind of struct inheritance but Rust doesn't have that at the moment which restricts us to use composition.
Any ideas?
The text was updated successfully, but these errors were encountered: