JSON parsing in Rust is safest when incoming text becomes a typed value at the edge of the program. Serde supplies the data-model traits, and serde_json applies those traits to JSON so application code can read fields instead of passing unchecked strings deeper into the codebase.

The usual binary-project setup is serde with the derive feature plus serde_json. A struct with #[derive(Deserialize)] defines the shape the program expects, and serde_json::from_str returns Result<T, serde_json::Error> instead of panicking when the JSON text is malformed or cannot deserialize into that shape.

An existing Cargo binary package is enough when JSON input belongs at a clear boundary, such as an API response, configuration file, or test fixture. A small service-configuration payload keeps the success and failure paths visible because valid text prints typed fields, while a trailing comma returns a parser error.

Steps to parse JSON with serde in Rust:

  1. Open a terminal in the Rust binary package root.

    The directory should contain the Cargo.toml file for the package. Create a package first if needed.
    Related: How to create a Rust project with Cargo

  2. Add serde with the derive macro feature.
    $ cargo add serde --features derive

    The derive feature provides #[derive(Deserialize)] for structs and enums in the package.
    Related: How to add a Rust dependency with Cargo

  3. Add the serde_json data format crate.
    $ cargo add serde_json

    serde defines the traits, while serde_json reads and writes JSON using those traits.

  4. Replace the binary entry point with a typed parser and an error branch.
    src/main.rs
    use serde::Deserialize;
     
    #[derive(Deserialize)]
    struct ServiceConfig {
        service: String,
        retries: u8,
        enabled: bool,
    }
     
    fn parse_config(input: &str) -> Result<ServiceConfig, serde_json::Error> {
        serde_json::from_str(input)
    }
     
    fn print_config(input: &str) {
        match parse_config(input) {
            Ok(config) => {
                println!(
                    "loaded service={} retries={} enabled={}",
                    config.service, config.retries, config.enabled
                );
            }
            Err(err) => println!("invalid JSON: {err}"),
        }
    }
     
    fn main() {
        let valid_json = r#"{"service":"billing","retries":3,"enabled":true}"#;
        let invalid_json = r#"{"service":"billing","retries":3,"enabled":true,}"#;
     
        print_config(valid_json);
        print_config(invalid_json);
    }

    The Rust field names must match the JSON object keys unless a serde field attribute, such as #[serde(rename = "jsonName")], maps a different key.

  5. Run the parser smoke test.
    $ cargo run --quiet
    loaded service=billing retries=3 enabled=true
    invalid JSON: trailing comma at line 1 column 49

    The first output line comes from the valid ServiceConfig value. The second line comes from the Err branch returned by serde_json::from_str.
    Tool: JSON Validator