Search code examples
jsonrustserdeserde-json

Convert serde_json Value keys to camelCase


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?


Solution

  • 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 - struson, e.g., but its API is awkward for this) 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)