-1

I'm writing a client library around this REST server. Octoprint (A server to manage a 3d printer), to be precise.
Here's one of the types i'm working with:

#[derive(Serialize, Deserialize, Debug)]
pub struct JobInfo {
    /// The file that is the target of the current print job
    pub file: FileInfo,
    /// The estimated print time for the file, in seconds
    #[serde(rename = "estimatedPrintTime")]
    pub estimated_print_time: Option<f64>,
    /// The print time of the last print of the file, in seconds
    #[serde(rename = "lastPrintTime")]
    pub last_print_time: Option<f64>,
    /// Information regarding the estimated filament usage of the print job
    pub filament: Option<Filament>,
}

Pretty straightforward, Using the multiplicity property defined in the specification of the API, I determined which properties should be considered optional, hence why some of these props are wrapped in options.

Unfortunately the documentation lies a little bit in the way multiplicity works here; here's an example on what a response looks like when the printer is in an offline state. For the sake of brevity, I will omit most of the body of this JSON message and keep just enough to get the point across

{
"job": {
    "file": null,
    "filepos": null,
    "printTime": null,
    ... etc
},
...
"state": "Offline"
}

Here's the type that I'm expecting for this response:

#[derive(Serialize, Deserialize, Debug)]
pub struct JobInformationResponse {
    /// Information regarding the target of the current print job
    job: JobInfo,
    /// Information regarding the progress of the current print job
    progress: ProgressInfo,
    /// A textual representation of the current state of the job
    /// or connection. e.g. "Operational", "Printing", "Pausing",
    /// "Paused", "Cancelling", "Error", "Offline", "Offline after error",
    /// "Opening serial connection" ... - please note that this list is not exhaustive!
    state: String,
    /// Any error message for the job or connection. Only set if there has been an error
    error: Option<String>,
}

Now I could just wrap all of these types in Options, but the previous example json wouldn't parse, since technically since job is an object, it's not going to deserialize as None despite the fact that each of it's keys are null. I was wondering if there were some sort of attribute in serde that would be able to handle this weird kind of serialization issue. I'd like to avoid just wrapping every single property in Options just to handle the edge case where the printer is offline

Edit: I guess what I'm trying to say is that I would expect that if all props on a struct in the json representation were null, that the object itself would serialize as None

Brandon Piña
  • 614
  • 1
  • 6
  • 20
  • I'm not quite sure whether I understood how `JobInfo` and your example message relate as they don't have the same fields. I'm also not quite sure i understand your pain: 3 out of 4 fields in `JobInfo` are already `Option`. – Caesar Feb 13 '23 at 07:11
  • Jobinfo is a sub field of the larger jobinformation response. And serde doesn't care if only one field is not optional. If file is empty on the file field of jobinfo then it's a serialization error – Brandon Piña Feb 13 '23 at 13:48
  • I don't understand your question, please be more clear, you talk about many json possibility but there is only one example, and there are 2 struct definition for the same thing. Be more clear, I'm stupid. What is the actual problem you have, it's very unclear. – Stargateur Feb 13 '23 at 14:09
  • Judging by the fact that someone was able to write a relevant answer to this question i'd say that the question is fine. Nested types shouldn't be a difficult concept. I'd suggest you take more care in reading the question – Brandon Piña Feb 13 '23 at 14:28
  • Judging by the fact the one who answer you also say your question is unclear, I maintain my point – Stargateur Feb 13 '23 at 18:11
  • @BrandonPiña Your question is indeed difficult to decode. I was able to guess that the `state` and `job` properties of your JSON must be part of the `JobInformationResponse`, then the content of the `job` field (`file`, `filepos`, `printTime`) must correspond to `JobInfo`, even if the field names don't match at all. It'd sure be nice to fix that at least. Also, e.g., delete `ProgressInfo`, it's irrelevant to the question. Oh, and proper JSON formatting would be nice. – Caesar Feb 14 '23 at 05:47

1 Answers1

1

If you're willing to redesign a little bit, you might be able to do something like this:

#[serde(tag = "state")]
enum JobInformationResponse {
    Offline {}
    // If a field only appears on one type of response, use a struct variant
    Error { error: String },
    // If multiple response types share fields, use a newtype variant and a substruct
    Printing(JobInformationResponseOnline),
    Paused(JobInformationResponseOnline),
    // ...
}
struct JobInformationResponseOnline {
    job: JobInfo,
    progress: ProgressInfo,
}

This works in the Offline case because by default, serde ignores properties that don't fit into any field of the struct/enum variant. So it won't check whether all entries of job are null.

If you have fields that appear in every message, you can further wrap JobInformationResponse (you should probably rename it):

struct JobInformationResponseAll {
    field_appears_in_all_responses: FooBar,
    #[serde(flatten)]
    state: JobInformationResponse // Field name doesn't matter to serde
}

But I'm not sure whether that works for you, since I certainly haven't seen enough of the spec or any real example messages.

To answer your question directly: No, there is no attribute in serde which would allow an all-null map to be de/serialized as None. You'd need two versions of the struct, one without options (to be used in your rust code) and one with (to be used in a custom deserialization function where you first deserialize to the with-options struct and then convert). Might not be worth the trouble.

And a side note: You might be happy to find #[serde(rename_all = "camelCase")] exists.

Caesar
  • 6,733
  • 4
  • 38
  • 44
  • Hmm. I think that's a good way to handle the "error" case since at least in that state the error field is mutually exclusive with everything else, but how does that work with the "offline" case? Since technically all the other fields are populated with objects that just happen to have all null properties. Would serde just figure out "hey I should check if this enum variant serializes at all" if the "online" variant is successful – Brandon Piña Feb 13 '23 at 14:02
  • I think I see what this is supposed to do. So instead of using the key of the property as the discrimminator we use the value of another property in the same level of the object. Unfortunately this doesn't really work for the error variant, as in the error case I believe that the "state" property is omitted. Other than that this looks like exactly what I was needing – Brandon Piña Feb 14 '23 at 04:11
  • Hm, I see two ways of dealing with `Error` without `state`. You could mark `Error` as `#[serde(other)]`, but I'm not sure that would work and not cause weird error messages in some edge cases. Another idea: you might have two enums: `#[serde(untagged)] enum ResponseOuter { Error { error: String }, Success(ResponseInner) } #[serde(tag = "state")] enum ResponseInner { Offline{}, Printing{ … }, … }`. Also, what happens for `Offline` not having any fields (post edited): all properties, regardless of content (`null` or not) are ignored. – Caesar Feb 14 '23 at 05:50
  • [Playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=90e40e8da1d1cd76fb09947b7a891a72) just to make sure this stuff actually works. – Caesar Feb 14 '23 at 05:53