REST API with Actix-Web - Partial JSON

I'm implementing my first REST API using Actix-Web, Diesel, and PostgreSQL. I have GET, POST, PUT, and DELETE working. The problem is implementing PATCH. Using a similar function to the PUT function, I get this response from cURL using just one field:

// Json deserialize error: invalid type: string "task", expected struct Entry at line 1 column 6

I understand that this is because it cannot construct the struct because the mandatory fields are not included in the cURL command. However, I do not know how to go about receiving a partial JSON string.

Apart from the PATCH page in the documentation, which doesn't seem to give a clue on how to implement it, I cannot seem to find any other information on this. I've searched the web, checked tutorials, and the actix-web git, and haven't found any reference to this - not even anyone else encountering the same issue.

Am I missing something obvious here?

Some research shows serde_json::Value::Object allows partial de-serialization, but the actix-web JSON<> type returns the error before I even have access to the data.

The only solution I can come up with is to make all fields Option<>, but would require manual checking of required fields, which makes this feel like a hack.

Any thoughts?

Can you further describe what you meant here? Option<T> is meant to be used precisely for optional fields; if the field is required, then do not wrap its type in an Option.

It might be helpful if you describe exactly what do you expect from your PATCH endpoint so that we can give you ideas on how to implement it.

Here's an example type:

pub struct Entry {
    pub id: usize,
    pub days: u32,
    pub task: String,
}

Creating a new entry:

curl -X POST -H "Content-Type: application/json" -d "{"days": "4", "task": "thing to do"}" http://localhost:8080/api/entries
// { "id": "1", "days": "4", "task": "thing to do" }

Modifying the entire entry with PUT:

curl -s -X PUT -H "Content-Type: application/json" -d "{"days": "4", "task": "another thing to do"}" http://localhost:8080/api/entries
// { "id": "1", "days": "4", "task": "another thing to do" }

Attempting to modify a single field with PATCH:

curl -s -X PATCH -H "Content-Type: application/json" -d "{"days": "7"}" http://localhost:8080/api/entries
// error message

(Edit: modified value)

I'm not familiar with Actix.

But from what's been posted, it seems to me that a version of your struct with all Optional fields (in addition to your normal struct) is precisely what's needed. To apply the patch, you only update fields that are not None.

If the caller has to supply all required fields, it's not a patch. If you fill in the non-supplied fields with some default value, how would you you distinguish between these?

# Rename task, preserve days
curl ... -d '{ "task": "foo" }'

# Rename task, set days to 0
curl ... -d '{ "task": "foo", "days": 0 }'
1 Like

I see. First of all, I recommend you to use different models for your REST API payloads than the ones used on the database. For example, you could have a PatchEntryPayload that has all the fields that Entry has (sans id, ofc), marked as optional. Having this separation of concerns will help you to avoid future headaches.

Another option which is better in my opinion, is to have an enum describing all the possible operations that you expect to happen when patching an Entry:

enum PatchEntry {
  RenameTask {
    new_name: String
  },
  ChangeDays {
    new_days: u32
  }
}

You can use Serde annotations to define the tag for the variant (i.e. it could be the field type) to be used to deserialize the payload.

This way, you are already defining your domain logic and making it impossible to change both the days and the task name, if that's an invariant that you would like to enforce.

4 Likes

Brilliant! It's clear now that this is absolutely the approach I should take.

I'm embarrassed to admit that I've actually done something similar in my code already, to handle query parameters. For some reason, I didn't even think of this approach for PATCH. I guess I was looking for a library-specific solution.

Thank you quinedot and moy2010!

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.