43

I am using the Jackson ObjectMapper to deserialize some JSON into a Java class, which we'll call PlayerData. I would like to add a bit of logic to the PlayerData class to fix up some data after the fields have been loaded in. For example, some early JSON files used to use a "sex" flag instead of a "gender" falg, so if the sex flag is set but the gender flag is not set, I'd like to set the value of the gender field to be the value of the sex field.

Is there some sort of @PostConstruct or @AfterLoad annotation that I could affix to a method? Or perhaps an interface that I could implement? I didn't notice one in the documentation, but it seemed like an obvious feature.

Brandon Yarbrough
  • 37,021
  • 23
  • 116
  • 145
  • 4
    [This](https://github.com/FasterXML/jackson-databind/issues/279#issuecomment-104538715) suggestion perfectly worked for me as replacement for `@PostConstruct` – fedor.belov Jun 11 '15 at 19:06

4 Answers4

25

Found this thru a link in the comments (credit: fedor.belov). This appears to allow you to run code post construct.

Adding a comment for people who end up here via http://jira.codehaus.org/browse/JACKSON-645 or http://jira.codehaus.org/browse/JACKSON-538 and are looking for a method which is called after a deserializer completes. I was able to achieve the desired effect by including an annotation and writing a converter which uses the same class as input and output.

@JsonDeserialize(converter=MyClassSanitizer.class)  // invoked after class is fully deserialized
public class MyClass {
    public String field1;
}

import com.fasterxml.jackson.databind.util.StdConverter;
public class MyClassSanitizer extends StdConverter<MyClass,MyClass> {
  @Override
  public MyClass convert(MyClass var1) {
    var1.field1 = munge(var1.field1);
    return var1;
  }
}
Sean Bright
  • 118,630
  • 17
  • 138
  • 146
Rylander
  • 19,449
  • 25
  • 93
  • 144
  • 1
    Worth noting that a `static` class might be needed instead. See https://stackoverflow.com/a/54605694/885922 – xlm Nov 30 '20 at 04:34
11

This is not supported out of the box, but you can easily create your @JsonPostDeserialize annotation for methods to be called after deserialization.

First, define the annotation:

/**
 * Annotation for methods to be called directly after deserialization of the object.
 */
@Target({ ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface JsonPostDeserialize {
}

Then, add the following registration and implementation code to your project:

public static void addPostDeserializeSupport(ObjectMapper objectMapper) {
    SimpleModule module = new SimpleModule();
    module.setDeserializerModifier(new BeanDeserializerModifier() {
        @Override
        public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDescription,
                JsonDeserializer<?> originalDeserializer) {
            return new CustomAnnotationsDeserializer(originalDeserializer, beanDescription);
        }
    });
    objectMapper.registerModule(module);
}

/**
 * Class implementing the functionality of the {@link JsonPostDeserialize} annotation.
 */
public class CustomAnnotationsDeserializer extends DelegatingDeserializer {
    private final BeanDescription beanDescription;

    public CustomAnnotationsDeserializer(JsonDeserializer<?> delegate, BeanDescription beanDescription) {
        super(delegate);
        this.beanDescription = beanDescription;
    }

    @Override
    protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
        return new CustomAnnotationsDeserializer(newDelegatee, beanDescription);
    }

    @Override
    public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
        Object deserializedObject = super.deserialize(p, ctxt);

        callPostDeserializeMethods(deserializedObject);
        return deserializedObject;
    }

    private void callPostDeserializeMethods(Object deserializedObject) {
        for (AnnotatedMethod method : beanDescription.getClassInfo().memberMethods()) {
            if (method.hasAnnotation(JsonPostDeserialize.class)) {
                try {
                    method.callOn(deserializedObject);
                } catch (Exception e) {
                    throw new RuntimeException("Failed to call @JsonPostDeserialize annotated method in class "
                            + beanDescription.getClassInfo().getName(), e);
                }
            }
        }
    }
}

Finally, modify your ObjectMapper instance with addPostDeserializeSupport, it will invoke all @JsonPostDeserialize annotated method of deserialized objects.

oberlies
  • 11,503
  • 4
  • 63
  • 110
  • Better answer. Also remember that the method you apply the annotation to needs to be `public`. – end-user Jun 04 '21 at 22:52
  • Great answer. Any hint about how to pass parameters to the method annotated with `@JsonPostDeserialize`? – kekolab Nov 24 '22 at 16:24
11

If you're not using the @JsonCreator, then Jackson will use the setter and getter methods to set the fields.

So if you define the following methods assuming that you have Sex and Gender enums:

@JsonProperty("sex")
public void setSex(final Sex sex) {
  this.sex = sex;
  if (gender == null) {
    gender = (sex == Sex.WOMAN) ? Gender.WOMAN : Gender.MAN;
  }
}

@JsonProperty("gender")
public void setGender(final Gender gender) {
  this.gender = gender;
  if (sex == null) {
    sex = (gender == Gender.WOMAN) ? Sex.WOMAN : Sex.MAN;
  }
}

it would work.

Update: You can find all of the annotations of Jackson library here.

Update2: Other solution:

class Example {
  private final Sex sex;
  private final Gender gender;

  @JsonCreator
  public Example(@JsonProperty("sex") final Sex sex) {
    super();
    this.sex = sex;
    this.gender = getGenderBySex(sex)
  }

  @JsonFactory
  public static Example createExample(@JsonProperty("gender") final Gender gender) {
    return new Example(getSexByGender(gender));
  }

  private static Sex getSexByGender(final Gender) {
    return (gender == Gender.WOMAN) ? Sex.WOMAN : Sex.MAN;
  }

  private static Gender getGenderBySex(final Sex) {
    return (sex == Sex.WOMAN) ? Gender.WOMAN : Gender.MAN;
  }

}
KARASZI István
  • 30,900
  • 8
  • 101
  • 128
  • You even don't need `JsonProperty` since it by default detects all getters (including `isSomething()`) and setters usable for serialization/deserialization. – pingw33n Jul 26 '11 at 21:13
  • @JsonConstructor isn't listed in that list of all annotations you provided :( – Brandon Yarbrough Jul 26 '11 at 22:53
  • 1
    Maybe he was thinking of @JsonCreator... which can be used to specify alternate constructor (or factory method within class) to use, and could be used for coordinating values of multiple fields. – StaxMan Jul 26 '11 at 23:53
  • 1
    AFAIK only one `@JsonCreator` annotated constructor is allowed per a class. So one `@JsonFactory` factorory method and one `@JsonCreator` constructor can solve the problem. – KARASZI István Jul 27 '11 at 06:31
4

This is something that has actually been suggested couple of times earlier. So maybe filing an RFE would make sense; there are multiple ways in which this could work: obvious ones being ability to annotate type (@JsonPostProcess(Processor.class)) and ability to register post-processor through Module API (so that there's basically a callback when Jackson constructs deserializer, to let module specify post-processor to use if any). But perhaps there are even better ways to do this.

StaxMan
  • 113,358
  • 34
  • 211
  • 239
  • This is the original one (not duplicate) http://jira.codehaus.org/browse/JACKSON-538 – Yves M. Oct 08 '14 at 09:04
  • 5
    Since Codehaus Jira is deprecated, I think this is the actively tracked one: https://github.com/FasterXML/jackson-databind/issues/279 – StaxMan Oct 08 '14 at 17:56