Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I parse JSON into a Map with lowercase keys using Jackson?

Tags:

java

json

jackson

I am using the Jackson (1.9.x) library to parse JSON into a Map:

ObjectMapper mapper = new ObjectMapper();
Map<String,Object> map = (Map<String,Object>) mapper.readValue(jsonStr, Map.class);

Is there a way to tell the Jackson parser to lowercase all the names of the keys? I tried using a Jackson PropertyNamingStrategy, but that didn't work - it only seems to be useful when it is getting mapped onto some bean, not a Map.

Clarifications:

  1. I do not want to have to precreate beans for the JSON - I only want dynamic Maps
  2. The JSON keys coming in will not be lowercase, but I want all the map keys to be lowercase (see example below)
  3. The JSON is rather large and heavily nested, so regular expression replacements of the incoming JSON or creating a new map manually after the Jackson parsing is not at all desired.

Incoming JSON:

{"CustName":"Jimmy Smith","Result":"foo","CustNo":"1234"}

The Java map would have:

"custname" => "Jimmy Smith"
"result" => "foo"
"custno" => "1234"

[UPDATE]: The answer I gave below doesn't fully solve the problem. Still looking for a solution.

like image 494
quux00 Avatar asked Sep 16 '13 22:09

quux00


2 Answers

(nb this solution is tested only with Jackson 2)

It's possible to do this by wrapping the JsonParser and simply applying .toLowerCase() to all field names:

private static final class DowncasingParser extends JsonParserDelegate {
    private DowncasingParser(JsonParser d) {
        super(d);
    }

    @Override
    public String getCurrentName() throws IOException, JsonParseException {
        if (hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
            return delegate.getCurrentName().toLowerCase();
        }
        return delegate.getCurrentName();
    }

    @Override
    public String getText() throws IOException, JsonParseException {
        if (hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
            return delegate.getText().toLowerCase();
        }
        return delegate.getText();
    }
}

You then have to have a custom JsonFactory to apply your wrapper, as in this test:

@Test
public void downcase_map_keys_by_extending_stream_parser() throws Exception {
    @SuppressWarnings("serial")
    ObjectMapper mapper = new ObjectMapper(new JsonFactory() {
        @Override
        protected JsonParser _createParser(byte[] data, int offset, int len, IOContext ctxt) throws IOException {
            return new DowncasingParser(super._createParser(data, offset, len, ctxt));
        }

        @Override
        protected JsonParser _createParser(InputStream in, IOContext ctxt) throws IOException {
            return new DowncasingParser(super._createParser(in, ctxt));
        }

        @Override
        protected JsonParser _createParser(Reader r, IOContext ctxt) throws IOException {
            return new DowncasingParser(super._createParser(r, ctxt));
        }

        @Override
        protected JsonParser _createParser(char[] data, int offset, int len, IOContext ctxt, boolean recyclable)
                throws IOException {
            return new DowncasingParser(super._createParser(data, offset, len, ctxt, recyclable));
        }
    });
    assertThat(
            mapper.reader(Map.class)
                    .with(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
                    .with(JsonParser.Feature.ALLOW_SINGLE_QUOTES)
                    .readValue("{CustName:'Jimmy Smith', CustNo:'1234', Details:{PhoneNumber:'555-5555',Result:'foo'} } }"),
            equalTo((Map<String, ?>) ImmutableMap.of(
                    "custname", "Jimmy Smith",
                    "custno", "1234",
                    "details", ImmutableMap.of(
                            "phonenumber", "555-5555",
                            "result", "foo"
                            )
                    )));
}
like image 62
araqnid Avatar answered Sep 19 '22 00:09

araqnid


I figured out one way to do it. Use a org.codehaus.jackson.map.KeyDeserializer, put it in a SimpleModule and register that module with the Jackson ObjectMapper.

import org.codehaus.jackson.map.KeyDeserializer;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.module.SimpleModule;
import org.codehaus.jackson.Version;

// ...

class LowerCaseKeyDeserializer extends KeyDeserializer {
  @Override
  public Object deserializeKey(String key, DeserializationContext ctx) 
      throws IOException, JsonProcessingException {
    return key.toLowerCase();
  }
}

// ...

ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule("LowerCaseKeyDeserializer", 
                                       new Version(1,0,0,null));
module.addKeyDeserializer(Object.class, new LowerCaseKeyDeserializer());
mapper.registerModule(module);
Map<String,Object> map = 
  (Map<String,Object>) mapper.readValue(jsonStr, Map.class);

[UPDATE]: Actually this only will lowercase the top level map keys, but not nested keys.

If the input is:

{"CustName":"Jimmy Smith","CustNo":"1234","Details":{"PhoneNumber": "555-5555", "Result": "foo"}}

The output in the map, unfortunately, will be:

{"custname"="Jimmy Smith", "custno"="1234", "details"={"PhoneNumber"="555-5555", "Result"="foo"}}
like image 25
quux00 Avatar answered Sep 19 '22 00:09

quux00