1

I'm writing a CLI tool that reads JSON files and is supposed to convert the JSON object keys into camelCase.

Because this should work with any JSON file, I obviously can't just use strong typing and then #[serde(rename_all = "camelCase")].

I can't seem to find an obvious way in serde_json to make it use the already existing renaming code that serde clearly has and apply it to a serde_json::value::Value.

Am I missing something obvious?

Luis Nell
  • 534
  • 4
  • 12
  • 1
    The renaming/lowercasing is done by the derive macro at compile time, not when parsing the JSON at runtime. See https://github.com/serde-rs/serde/blob/master/serde_derive/src/internals/case.rs#L12-L84 – linuskmr Oct 07 '22 at 11:22

1 Answers1

2

You'll have to write a function that recurses through the serde_json::Value structure and replaces the keys of serde_json::Map whenever it encounters one. That's a bit awkward to implement, as there is no Map::drain.

fn rename_keys(json: &mut serde_json::Value) {
    match json {
        serde_json::Value::Array(a) => a.iter_mut().for_each(rename_keys),
        serde_json::Value::Object(o) => {
            let mut replace = serde_json::Map::with_capacity(o.len());
            o.retain(|k, v| {
                rename_keys(v);
                replace.insert(
                    heck::ToLowerCamelCase::to_lower_camel_case(k.as_str()),
                    std::mem::replace(v, serde_json::Value::Null),
                );
                true
            });
            *o = replace;
        }
        _ => (),
    }
}

use std::io::Read;
fn main() {
    let mut stdin = vec![];
    std::io::stdin()
        .read_to_end(&mut stdin)
        .expect("Read stdin");
    let mut json = serde_json::from_slice::<serde_json::Value>(&stdin).expect("Parse Json");
    rename_keys(&mut json);
    println!("{}", serde_json::to_string_pretty(&json).unwrap());
}

(Note that rename_keys will produce a stack overflow on deep JSON structures, but serde_json only parses to a limited depth by default, so no need to worry. If you do need support for deeply nested structures, have a look at serde_stacker.)


If you're not interested in the serde_json::Value itself and just want to transform a JSON string, there's two more ways to go on about this:

  • You could do the renaming on serialization, by writing a custom serializer for a wrapper struct around serde_json::Value. An example of such a serializer is here, but you'd have to adopt it to be recursive. (Possibly, doing it at deserialization might be easier than at serialization)
  • Write a JSON tokenizer (or grab a crate that contains one) to skip creating the actual serde_json::Value structure and to the renaming on the token stream (no need to worry when working with GBs of JSON)
Caesar
  • 6,733
  • 4
  • 38
  • 44
  • Ok great. That confirms that I was not missing anything obvious :) Thank you so much for the very complete answer that covers all complexities and then some more! – Luis Nell Oct 11 '22 at 15:34