[SOLVED]: Deserializing JSON with actix-web fails

I started implementing my first API with rust. It looks like Actix-web is a very popular framework, so I thought to give it a spin.

However, linking my data types with the handlers isn't working yet.

I assume there are several issues here, so please bear with me.

First my types:

#[derive(Serialize, Deserialize)]
pub struct Reservation {
    pub id: i64,
    pub pub_key: Vec<u8>,
    pub expiry: u64,
    pub terms: Vec<u8>,
    pub sig: Vec<u8>,
    pub range: Range,
}

#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct Range {
    pub id: u64,
    pub expiry: Option<u64>,
    pub terms: Option<Vec<u8>>,
    pub slots: Vec<Slot>,
}

#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct Slot {
    pub id: u64,
    pub app_id: u64,
    pub reserved: Option<Vec<u8>>,
    pub consumed: bool,
    pub expiry: Option<u64>,
    pub terms: Option<Vec<u8>>,
}

I am already not sure how internally the deserialization of Vec<u8> has to work. Also, another doubt is on Option fields.

One of my handler looks like this:

pub async fn reserve(db: web::Data<ReserveDB>, item: web::Json<Reservation>) -> HttpResponse {
    let reservation = item.into_inner();
    match db.save_reservation(&reservation) {
    // prepare response   
    }
}

The server is started like this:

    #[actix_web::main]
    pub async fn start(&self) -> std::io::Result<()> {
        let settings = default_settings().expect("settings should have been set up");
        let reserve_db = match ReserveDB::new(settings) {
           // handle setting up db
        };

        HttpServer::new(move || {
            App::new()
                .wrap(Logger::default())
                .app_data(web::Data::new(reserve_db.clone()))
                .service(
                    web::scope("/api/v1")
                        .route("/reservation", web::put().to(handlers::reserve))
                )
        })
        .bind("127.0.0.1:8080")?
        .workers(2)
        .run()
        .await
    }

So far, every attempt to call this via curl failed with a deserialization error:

2025-05-11T22:45:32Z DEBUG actix_web::types::json] Failed to deserialize Json from payload. Request path: /api/v1/reservation
[2025-05-11T22:45:32Z DEBUG actix_web::middleware::logger] Error in response: ContentType
[2025-05-11T22:45:32Z INFO  actix_web::middleware::logger] 127.0.0.1 "PUT /api/v1/reservation HTTP/1.1" 400 18 "-" "curl/8.13.0" 0.000238

As you can see it seems as that I am also responding incorrectly. But if that is not relevant to this question, let's leave that out for now. I want to understand why it is not able to deserialize json correctly.

Here is one of the attempts at calling the endpoint, with a fully expanded Reservation struct:

curl -v -X PUT -d '{"id":-1,"pub_key":[1,2,3,4],"expiry":0,"terms":[33,33],"sig":[6,5,4,3],"range":{"id":1,"expiry":0,"terms":[1,2,3,3],"slots":[{"id":0,"app_id":42,"reserved":"","consumed":false,"expiry":0,"terms":[0]}]}}' -H: "Content-type: application/json" localhost:8080/api/v1/reservation

The byte values really don't matter for now, I am just interested to get the deserialization going right first. Is that the correct way of putting byte values in JSON?

I have also tried with just leaving out the nested struct fields:

curl -v -X PUT -H: "Content-type: application/json" -d '{"id":-1,"pub_key":[1,2,3,4],"expiry":0,"terms":[33,33],"sig":[6,5,4,3],"range":{}}' localhost:8080/api/v1/reservation

One of the other numerous attempts has involved "prefixing" the JSON object with the struct name:
-d {"reservation":{...}}

But that also didn't help.
I couldn't find much documentation in the serde docs specifically about byte vectors (did I just not look right?), but maybe I am not getting something way more basic yet. Or maybe I am oversimplifying things from looking at the (usual) simple example in the docs.

Can someone shed light here please? Thanks.

Holy moly after attempt #412 or so I got the gist.

First, of course, because Rust doesn't handle empty fields, I need to provide a complete structure.

Second, my curl was bad. I was setting the header at the end and also malformed.

The third issues was the reserved field, which I was setting to an empty string. Being it a Vec<u8> type, that did not work either.

So this works:
curl -v -X POST -H "Content-Type: application/json" -d '{"id":-1,"pub_key":[1,2,3,4],"expiry":0,"terms":[33,33],"sig":[6,5,4,3],"range":{"id":1,"expiry":0,"terms":[1,2,3,3],"slots":[{"id":0,"app_id":42,"reserved":[],"consumed":false,"expiry":0,"terms":[0]}]}}' -H: "Content-type: application/json" localhost:8080/api/v1/reservation

Oh, and I also removed the item.into_inner() call, looking at the docs I could conclude that the object already comes fully deserialized.

This should get me going for next steps. Sorry for this post, but maybe it's useful to others.

If you want to optionally provide data fields, you should put them into an Option enum, as you already have done with some of the values. Depending on your use case, for container types, such as Vec<_> there is the #[serde(default)] field attribute to allow serde to default a missing value to an empty collection. E.g.:

#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct Range {
    pub id: u64,
    pub expiry: Option<u64>,
    #[serde(default)]
    pub terms: Vec<u8>,
    #[serde(default)]
    pub slots: Vec<Slot>,
}
1 Like