You can try to implement custom deserialiser with com.fasterxml.jackson.databind.deser.ContextualDeserializer interface. It allows to create deserialiser instance with a context.
See below example:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.json.JsonMapper;
import lombok.Data;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class JsonContextualDeserializerApp {
public static void main(String[] args) throws IOException {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
ObjectMapper mapper = JsonMapper.builder().build();
RelationsInput info = mapper.readValue(jsonFile, RelationsInput.class);
System.out.println(info.toString());
}
}
@Data
class RelationsInput {
@JsonDeserialize(using = RelationStdDeserializer.class)
@RelationTypeInfo(RelationType.OWNER)
private Relation owner;
@JsonDeserialize(using = RelationStdDeserializer.class)
@RelationTypeInfo(RelationType.TENANT)
private Relation tenant;
}
@Data
class Relation {
private int id;
private RelationType relationtype;
}
enum RelationType {OWNER, TENANT}
@Retention(RetentionPolicy.RUNTIME)
@interface RelationTypeInfo {
RelationType value();
}
class RelationStdDeserializer extends StdDeserializer<Relation> implements ContextualDeserializer {
private RelationType propertyRelationType;
public RelationStdDeserializer() {
this(null);
}
public RelationStdDeserializer(RelationType relationType) {
super(Relation.class);
this.propertyRelationType = relationType;
}
@Override
public Relation deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonDeserializer<Object> deser = ctxt.findRootValueDeserializer(ctxt.getTypeFactory().constructType(Relation.class));
Relation instance = (Relation) deser.deserialize(p, ctxt);
if (this.propertyRelationType != null) {
instance.setRelationtype(this.propertyRelationType);
}
return instance;
}
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
RelationTypeInfo typeInfo = property.getMember().getAllAnnotations().get(RelationTypeInfo.class);
return new RelationStdDeserializer(typeInfo.value());
}
}
Above code for a payload:
{
"owner": {
"id": 1
},
"tenant": {
"id": 2
}
}
prints:
RelationsInput(owner=Relation(id=1, relationtype=OWNER), tenant=Relation(id=2, relationtype=TENANT))
See also:
- Deserialize to String or Object using Jackson
- How to inject dependency into Jackson Custom deserializer
- Jackson - deserialize inner list of objects to list of one higher level
You can try to implement custom deserialiser with com.fasterxml.jackson.databind.deser.ContextualDeserializer interface. It allows to create deserialiser instance with a context.
See below example:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.json.JsonMapper;
import lombok.Data;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class JsonContextualDeserializerApp {
public static void main(String[] args) throws IOException {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
ObjectMapper mapper = JsonMapper.builder().build();
RelationsInput info = mapper.readValue(jsonFile, RelationsInput.class);
System.out.println(info.toString());
}
}
@Data
class RelationsInput {
@JsonDeserialize(using = RelationStdDeserializer.class)
@RelationTypeInfo(RelationType.OWNER)
private Relation owner;
@JsonDeserialize(using = RelationStdDeserializer.class)
@RelationTypeInfo(RelationType.TENANT)
private Relation tenant;
}
@Data
class Relation {
private int id;
private RelationType relationtype;
}
enum RelationType {OWNER, TENANT}
@Retention(RetentionPolicy.RUNTIME)
@interface RelationTypeInfo {
RelationType value();
}
class RelationStdDeserializer extends StdDeserializer<Relation> implements ContextualDeserializer {
private RelationType propertyRelationType;
public RelationStdDeserializer() {
this(null);
}
public RelationStdDeserializer(RelationType relationType) {
super(Relation.class);
this.propertyRelationType = relationType;
}
@Override
public Relation deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
JsonDeserializer<Object> deser = ctxt.findRootValueDeserializer(ctxt.getTypeFactory().constructType(Relation.class));
Relation instance = (Relation) deser.deserialize(p, ctxt);
if (this.propertyRelationType != null) {
instance.setRelationtype(this.propertyRelationType);
}
return instance;
}
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
RelationTypeInfo typeInfo = property.getMember().getAllAnnotations().get(RelationTypeInfo.class);
return new RelationStdDeserializer(typeInfo.value());
}
}
Above code for a payload:
{
"owner": {
"id": 1
},
"tenant": {
"id": 2
}
}
prints:
RelationsInput(owner=Relation(id=1, relationtype=OWNER), tenant=Relation(id=2, relationtype=TENANT))
See also:
- Deserialize to String or Object using Jackson
- How to inject dependency into Jackson Custom deserializer
- Jackson - deserialize inner list of objects to list of one higher level
I am afraid there is not such thing. If you want to use something annotation like you could use custom de-serializers. Create first something like:
@RequiredArgsConstructor
public abstract class RelationTypeDeserializer extends JsonDeserializer<Relation> {
private final RelationType relationType;
@Override
public Relation deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
Relation r = p.readValueAs(Relation.class);
r.setRelationtype(relationType);
return r;
}
}
Then implement the actual ones:
public class OwnerDeserializer extends RelationTypeDeserializer {
public OwnerDeserializer() {
super(RelationType.OWNER);
}
}
and
public class TenantDeserializer extends RelationTypeDeserializer {
public TenantDeserializer() {
super(RelationType.TENANT);
}
}
then use those like:
@Getter @Setter
public class RelationsInput {
@JsonDeserialize(using = OwnerDeserializer.class)
private Relation owner;
@JsonDeserialize(using = TenantDeserializer.class)
private Relation tenant;
}
Method hasIgnoreMarker is called not only for fields, but also for the constructor, including the virtual one:
Method called to check whether given property is marked to be ignored. This is used to determine whether to ignore properties, on per-property basis, usually combining annotations from multiple accessors (getters, setters, fields, constructor parameters).
In this case you should ignore only fields, that are not marked properly:
static class CustomAnnotationIntrospector extends JacksonAnnotationIntrospector {
@Override
public PropertyName findNameForDeserialization(Annotated a) {
Property property = a.getAnnotation(Property.class);
if (property == null) {
return PropertyName.USE_DEFAULT;
} else {
return PropertyName.construct(property.name());
}
}
@Override
public boolean hasIgnoreMarker(AnnotatedMember m) {
return m instanceof AnnotatedField
&& m.getAnnotation(Property.class) == null;
}
}
Example:
class Pojo {
// @Property(name = "id")
Integer id;
// @Property(name = "number")
Integer number;
@Property(name = "assure")
Boolean assure;
@Property(name = "person")
Map<String, String> person;
}
String json =
"{\"id\" : 1, \"number\" : 12345, \"assure\" : true," +
" \"person\" : {\"name\" : \"John\", \"age\" : 23}}";
ObjectMapper mapper = new ObjectMapper();
mapper.setAnnotationIntrospector(new CustomAnnotationIntrospector());
Pojo pojo = mapper.readValue(json, Pojo.class);
System.out.println(pojo);
Pojo{id=null, number=null, assure=true, person={name=John, age=23}}
Note: Custom Property annotation should have RetentionPolicy.RUNTIME (same as JsonProperty annotation):
@Retention(RetentionPolicy.RUNTIME)
public @interface Property {
String name();
}
I will suggest a different approach:
In the runtime, with the bytecode instrumentation library Byte Buddy and its Java agent, re-annotate the fields with the proper Jackson Annotations. Simply implement the logic via reflection. See the following example:
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.dynamic.DynamicType.Builder;
import net.bytebuddy.dynamic.DynamicType.Builder.FieldDefinition.Valuable;
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy;
import net.bytebuddy.matcher.ElementMatchers;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface MyJsonIgnore {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@interface MyJsonProperty {
String name();
}
public class Sample {
public static void main(String[] args) throws JsonProcessingException {
ByteBuddyAgent.install();
ClassReloadingStrategy classReloadingStrategy = ClassReloadingStrategy.fromInstalledAgent();
ByteBuddy byteBuddy = new ByteBuddy();
AnnotationDescription jsonIgnoreDesc =
AnnotationDescription.Builder.ofType(JsonIgnore.class).build();
Builder<Person> personBuilder = byteBuddy.redefine(Person.class);
for (Field declaredField : Person.class.getDeclaredFields()) {
Valuable<Person> field = personBuilder.field(ElementMatchers.named(declaredField.getName()));
MyJsonProperty myJsonProperty = declaredField.getAnnotation(MyJsonProperty.class);
if (myJsonProperty != null) {
AnnotationDescription jsonPropertyDesc =
AnnotationDescription.Builder.ofType(JsonProperty.class)
.define("value", myJsonProperty.name())
.build();
personBuilder = field.annotateField(jsonPropertyDesc);
}
MyJsonIgnore myJsonIgnore = declaredField.getAnnotation(MyJsonIgnore.class);
if (myJsonIgnore != null) {
personBuilder = field.annotateField(jsonIgnoreDesc);
}
}
personBuilder.make().load(Sample.class.getClassLoader(), classReloadingStrategy);
Person person = new Person("Utku", "Ozdemir", "Berlin");
ObjectMapper objectMapper = new ObjectMapper();
String jsonString = objectMapper.writeValueAsString(person);
System.out.println(jsonString);
}
}
class Person {
@MyJsonProperty(name = "FIRST")
private String firstName;
@MyJsonProperty(name = "LAST")
private String lastName;
@MyJsonIgnore private String city;
public Person(String firstName, String lastName, String city) {
this.firstName = firstName;
this.lastName = lastName;
this.city = city;
}
}
In the example above, I
- create
MyJsonPropertyandMyJsonIgnoreannotations and aPersonclass for the demonstration purpose - instrument the current Java process with the Byte buddy agent
- create a bytebuddy builder to redefine the
Personclass - loop over the fields of the
Personclass and check for these annotations - add an additional annotation to each of those fields (on the builder), Jackson's
JsonProperty(with the correct field name mapping) andJsonIgnore. - after being done with the fields, make the new class bytecode and load it to the current classloader using the byte buddy agent's class reloading mechanism
- write a person object to the stdout.
It prints, as expected:
{"FIRST":"Utku","LAST":"Ozdemir"}
(the field city is ignored)
This solution might feel like an overkill, but on the other side, it is pretty generic solution - with a few changes in the logic, you could handle all the 3rd party classes (which you are not able to modify) instead of handling them case by case.