Search code examples
javajsonjacksondeserializationjson-deserialization

Deserializing json embedded node as value into Map using Jackson


Dear folks I need you advice.

I'm, trying to implement custom Jackson deserializer for maps but having difficulties with it. As an input data I have the following json:

{
    "someMap": {
        "one_value": "1",
        "another_value: "2"
    },
    "anotherMap": "{\"100000000\": 360000,\"100000048\": 172800,\"100000036\": 129600,\"100000024\": 86400,\"100000012\": 43200}"
}

As you can see in the second case it has json map inside node value(I'm doing it in that way by intention. Because I want to replace the value from env variable: "anotherMap": "${SOME_MAP:-{\"100000000\": 360000,\"100000048\": 172800,\"100000036\": 129600,\"100000024\": 86400,\"100000012\": 43200}}"). As I understand I have to differentiate somehow between that 2 maps deserialization flows. So for the first map I need to use default one map deserializer for the second one the custom one to have properly parsed map from the value. At the moment I wrote that code to do that:

// invokation code
new ObjectMapper().registerModule(new ConfigModule()).readValue(is, ConfigModuleTestConfigWrapper.class);

// module code
public class ConfigModule extends SimpleModule {

@Override
public void setupModule(SetupContext context) {
    super.setupModule(context);
    context.addDeserializers(new Deserializers.Base() {

          @Override
          public JsonDeserializer<?> findMapDeserializer(MapType type, DeserializationConfig config, BeanDescription beanDesc,
                                                         KeyDeserializer keyDeserializer, TypeDeserializer elementTypeDeserializer,
                                                         JsonDeserializer<?> elementDeserializer) throws JsonMappingException {
              return new MapPropertyDeserializer(type);
          }
    });
}

private static class MapPropertyDeserializer extends StdScalarDeserializer<Map<String, Integer>> {
    MapPropertyDeserializer(MapType type) {
        super(type);
    }
    @Override
    public Map<String, Integer> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        JsonNode node = p.readValueAsTree();
        if (node == null || node.isContainerNode()) {
              // return <default jackson deserializer>
        }
        System.out.println("isContainerNode ?: " + node.isContainerNode());
        System.out.println("isValueNode ?: " + node.isValueNode());
        // some parsing flow goes below
        JsonNode valueNode = node.get(1);
        valueNode.asText();
        return new HashMap<>();
    }
}

// bean description
@JsonIgnoreProperties
public class ConfigSubstitutorModuleTestConfigWrapper {

  private final Map<String, String> someMap;
  private final Map<String, Integer> anotherMap;

  @JsonCreator
  public ConfigSubstitutorModuleTestConfigWrapper(
          @JsonProperty("someMap") Map<String, String> someMap,
          @JsonProperty("anotherMap") Map<String, Integer> anotherMap

  ) {
      this.someMap = someMap;
      this.anotherMap = anotherMap;
  }

  public Map<String, String> getSomeMap() {
      return someMap;
  }
  public Map<String, Integer> getAnotherMap() {
      return anotherMap;
  }
}

The problem is(as I understand:)) that I don't know how to return default map deserializer from the deserialize method.

Does anybody have any clue what I could do there to achieve desired goal ?


Solution

  • Finally accepted that Solution to resolve it:

    1) Create deserializer class:

     /**
     * The target of that deserializer is to do two-step deserialization.
     * At first it just reads string and then does second deserialization in the proper {@link Map} type once string substitution done.
     * <p>
     * Note! In order to get object mapper reference you have to set it first on object mapper initialization stage:
     * </p>
     * <pre>
     *     objectMapper.setInjectableValues(new InjectableValues.Std().addValue(OBJECT_MAPPER_VALUE_ID, objectMapper));
     * </pre>
     */
    public class ValueAsMapDeserializer extends JsonDeserializer<Map> implements ContextualDeserializer {
        public static final String OBJECT_MAPPER_VALUE_ID = "objectMapper";
        static final String VALUE_PREFIX = "$|";
        static final String VALUE_SUFFIX = "|";
    
        private JavaType keyType;
        private JavaType valueType;
    
        @Override
        public JsonDeserializer<?> createContextual(final DeserializationContext ctxt,
                                                    final BeanProperty property) throws JsonMappingException {
            JavaType filedType = property.getType();
            this.keyType = filedType.getKeyType();
            this.valueType = filedType.getContentType();
            return this;
        }
    
        @Override
        public Map deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
            // Can't use constructor init there because of intention to use that deserializer using annotation
            // Also such tricky thing as 'injectable values' was used cause of no way to get the reference to object mapper from deserialization context out of the box
            ObjectMapper objectMapper = (ObjectMapper) ctxt.findInjectableValue(OBJECT_MAPPER_VALUE_ID, null, null);
            final Optional<String> substitutedValue = Substitutor.create(jp, VALUE_PREFIX, VALUE_SUFFIX).substitute();
            MapType mapType = objectMapper.getTypeFactory().constructMapType(Map.class, keyType, valueType);
            return objectMapper.readValue(substitutedValue.orElseThrow(() -> new RuntimeException("Failed to parse the value as map")), mapType);
        }
    }
    

    2) Mark bean field to use that deserializer:

    @JsonDeserialize(using = ValueAsMapDeserializer.class)
    private final Map<String, Integer> anotherMap;