Skip to Content
Course content

49: Handling JSON and Routing

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this exact scenario play out in countless code reviews. You've set up your Axum or Actix routes, you've defined your data structures, and you're sending a perfectly valid JSON object from your frontend. But for some reason, your server keeps screaming 400 Bad Request, and you're staring at your code wondering where the typo is.

use axum::{routing::post, Json, Router};
use serde::Deserialize;

#[derive(Deserialize)]
struct CreateUser {
    username: String,
    email_address: String,
}

async fn create_user(Json(payload): Json<CreateUser>) -> String {
    format!("User {} created!", payload.username)
}

// ... router setup ...

The Mystery of the 400 Bad Request

On the surface, that code looks bulletproof. But here is the catch: most frontend frameworks (React, Vue, etc.) and JavaScript APIs default to camelCase. Your frontend is likely sending {"username": "rustacean", "emailAddress": "rust@example.com"}.

Rust, however, follows snake_case. Serde, the engine doing the heavy lifting here, is strict. It looks for a field called email_address, doesn't find it because the JSON has emailAddress, and decides the request is malformed. It doesn't guess; it just fails. If you aren't logging the specific deserialization error, you're left guessing why a "valid" JSON object is being rejected.

Bridging the Gap with Serde Attributes

You could manually rename every single field using #[serde(rename = "emailAddress")], but that's a tedious waste of your time and makes your structs look cluttered. Instead, use the container attribute to tell Serde to translate the casing for the entire struct.

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")] // This is the magic line
struct CreateUser {
    username: String,
    email_address: String, 
}

Now, Serde knows that when it sees emailAddress in the JSON, it should map it to email_address in your Rust struct. I always recommend adding this by default if you're building a public-facing API; it saves you from a dozen annoying bugs down the road.

Connecting Dynamic Routes to JSON Payloads

Now that we can actually parse the data, let's talk about routing. In a real app, you're rarely just hitting a flat /create-user endpoint. You're usually dealing with resources, like /users/:id/update.

The trick here is combining Path extractors with Json extractors. Order matters in some frameworks, but in Axum, the general rule is that you can have multiple extractors, but only one "body" extractor (like Json), and it must come last. If you try to put Path after Json, you'll find the compiler complaining or the request hanging.

use axum::{extract::{Path, Json}, routing::patch, Router};
use serde::Deserialize;

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct UpdateUser {
    display_name: Option<String>,
    bio: Option<String>,
}

async fn update_user_profile(
    Path(user_id): Path<u64>, 
    Json(payload): Json<UpdateUser>
) -> String {
    format!("Updating user {} with name {:?}", user_id, payload.display_name)
}

// In your main function:
// .route("/users/:id/update", patch(update_user_profile))

Notice I used Option<String> for the update fields. This is a crucial pattern for JSON APIs. By using Option, you allow the client to send a partial update. If they only want to change the bio, they can omit displayName, and Serde will simply set that field to None rather than failing the entire request.




📋 Practical Task

Build a Product Inventory Management Endpoint

Your goal is to create a specialized route for an inventory system. You need to implement a PUT handler that updates a specific product's details.

  • The Route: /inventory/:product_id
  • The Payload: The frontend sends JSON in camelCase with the fields productName (String) and stockQuantity (u32).
  • The Requirements:
    1. Define a ProductUpdate struct that correctly maps the camelCase JSON to snake_case Rust fields.
    2. Create a handler function that extracts both the product_id from the URL path and the ProductUpdate data from the JSON body.
    3. The handler should return a string confirming the update, including both the ID and the new product name.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.