How to nest field in custom struct serializer?

Viewed 237

Given the following struct definition.

#[derive(serde::Serialize, serde::Deserialize, Debug)]
pub (in crate) struct ResponseError {
    pub status: StatusCode,
    pub title: String,
    pub message: String,
    pub trace: Option<String>,
}

How to group some of the fields into a namespace (nest them) during serialization? For example, grouping the title, message, and trace fields into error namespace like the following.

{
    "status": 0,
    "error": {
        "title": "",
        "message": "",
        "trace": "",
    },
}

The goal is to be able to create a flat struct in Rust while outputting structured response. This is the approach I can came up with.

impl Serialize for ResponseError {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let mut state = serializer.serialize_struct("Color", if self.trace.is_some() { 4 } else { 3 })?;
        state.serialize_field("status", &self.status)?;
        state.serialize_field("error.title", &self.title)?;
        state.serialize_field("error.message", &self.message)?;
        if let Some(trace) = self.trace {
            state.serialize_field("error.trace", &self.trace)?;
        };
        state.end()
    }
}
2 Answers

One way to approach this is to define separate structs for your "wire format", then provide implementations of From<> to convert to and from the wire-formats, and then finally use serde's from and into to do the final conversions.

The implementation of the wire structs and the From implementations are a little tedious - but simple:

use serde::{Serialize,Deserialize};

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(from = "ResponseErrorWireFormat", into = "ResponseErrorWireFormat")]
pub struct ResponseError {
    pub status: String,
    pub title: String,
    pub message: String,
    pub trace: Option<String>,
}

#[derive(Serialize, Deserialize)]
pub struct ResponseErrorInfoWireFormat {
    pub title: String,
    pub message: String,
    pub trace: Option<String>, 
}

#[derive(Serialize, Deserialize)]
pub struct ResponseErrorWireFormat {
    pub status: String,
    pub info: ResponseErrorInfoWireFormat
}

impl From<ResponseErrorWireFormat> for ResponseError {
    fn from(v: ResponseErrorWireFormat) -> ResponseError { 
        ResponseError {
            status: v.status,
            title: v.info.title,
            message: v.info.message,
            trace: v.info.trace,
        }
    }
}

impl From<ResponseError> for ResponseErrorWireFormat {
    fn from(v: ResponseError) -> ResponseErrorWireFormat { 
        ResponseErrorWireFormat {
            status: v.status,
            info: ResponseErrorInfoWireFormat {
                title: v.title,
                message: v.message,
                trace: v.trace,
            }
        }
    }
}

Then the code to use it is simple:

fn main() {
    let v = ResponseError {
        status: "an error".to_string(),
        title: "an error title".to_string(),
        message: "oh my, an error!".to_string(),
        trace: Some("it happened right here.".to_string()),
    };

    let serialized = serde_json::to_string(&v).unwrap();
    println!("serialized = {}", serialized);
    let deserialized: ResponseError = serde_json::from_str(&serialized).unwrap();
    println!("deserialized = {:?}", deserialized);
}

The complete example can be found here

This is more like the "anti-thesis" to your method. You can group those keys in a separate struct and use serde(flatten) to flatten the response.

#[derive(Serialize, Deserialize)]
struct ErrorInfo {
    pub title: String,
    pub message: String,
    pub trace: Option<String>,
}

#[derive(Serialize, Deserialize)]
struct ErrorResponse {
    pub status: StatusCode,

    #[serde(flatten)]
    error: ErrorInfo,
}

Check out the doc on Struct flattening here https://serde.rs/attr-flatten.html.

Related