12

I have a complex JSON file and I'd like to extract only a single value out of it. I could define all of the structs and derive Deserialize on all of them, but I'd like to just write a little manual code to pull that one value out. The Serde documentation, quite frankly, just confused me.

My JSON content has the following layout:

{
  "data": [
    {
      "hostname": "a hostname"
    }
  ]
}

I'm looking for the value navigated to by going into data, then taking the first element of the array, and taking the value of hostname.

In Haskell, I'd do it like this:

newtype Host = Host Text

instance FromJSON Host where
    parseJSON (Object o) = (return . Host) <=< (.: "hostname") <=< (fmap (!! 0) . parseJSON) <=< (.: "data") $ o
    parseJSON _ = mzero

What's the equivalent for Serde?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Listerone
  • 1,381
  • 1
  • 11
  • 25
  • That link is for ppl that want to implement a format deserializer/parser. [This](https://serde.rs/deserialize-map.html) should be what you need. – chpio May 23 '19 at 16:51
  • https://github.com/serde-rs/json#operating-on-untyped-json-values – turbulencetoo May 23 '19 at 17:39

3 Answers3

16

serde_json provides types for generic JSON values with serde_json::Value:

use serde_json::Value;

// input variable
let input: &str = "{...}";

// parse into generic JSON value
let root: Value = serde_json::from_str(input)?;

// access element using .get()
let hostname: Option<&str> = root.get("data")
    .and_then(|value| value.get(0))
    .and_then(|value| value.get("hostname"))
    .and_then(|value| value.as_str());

// hostname is Some(string_value) if .data[0].hostname is a string,
// and None if it was not found
println!("hostname = {:?}", hostname); // = Some("a hostname")

(full playground example)

Frxstrem
  • 38,761
  • 9
  • 79
  • 119
7

If you are OK with returning Value::Null if the value is missing or malformed, I'd use the Index syntax ([...]). If you want to handle the missing / malformed case differently, use the get method:

fn main() {
    let json_value = serde_json::json!({
      "data": [
        {
          "hostname": "a hostname"
        }
      ]
    });

    let host = &json_value["data"][0]["hostname"];
    println!("Host: {:?}", host);
}

You can also use a JSON Pointer via pointer:

let host = json_value.pointer("/data/0/hostname");
println!("Host: {:?}", host);
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • Do you recommend using `pointer` or using the index syntax? Or `.get(...)`? – Jake Ireland Aug 02 '21 at 01:40
  • 1
    @JakeIreland It depends on if you expect the value to be there or not (and thus using something that could panic). – Shepmaster Aug 02 '21 at 13:35
  • 1
    This should be the answer, except that the index does not panic according to the implementation and the docs: https://docs.rs/serde_json/1.0.62/src/serde_json/value/index.rs.html#175-213 – Raz0rwire Apr 06 '23 at 11:26
2

I would chain flattened structures

use serde::{Serialize, Deserialize};
use serde_json::Value;

#[derive(Serialize, Deserialize)]
struct Payload {
    data: Vec<Data>,

    #[serde(flatten)]
    _: HashMap<String, Value>,
}

#[derive(Serialize, Deserialize)]
struct Data {
    hostname: String,

    #[serde(flatten)]
    _: HashMap<String, Value>,
}

let payload: Payload = serde_json::from_str(your_string)?;
assert_eq!(payload.data.0.hostname, "a hostname");
gustavodiazjaimes
  • 2,441
  • 1
  • 19
  • 14
  • There is really no need for your hashmap, funny enough this answer is the best from performance point of view. Use json::Value is very slow. But remove your useless hashmap. – Stargateur Sep 20 '22 at 16:34