How to conditionally serialize a field - java

I want to conditionally serialize a field. I figured out how to conditionally ignore a field with
public class NologIntrospector extends JacksonAnnotationIntrospector {
#Override
public boolean hasIgnoreMarker(AnnotatedMember m) {
boolean noLogOnClass = m.getDeclaringClass().getAnnotation(NoLog.class) != null;
return m.hasAnnotation(NoLog.class) || super.hasIgnoreMarker(m) || noLogOnClass;
}
}
But what I really want to do is to redact the field. So if I have
#Getter
#Setter
#NoArgsConstructor
public class MyObject1 {
public String field1 = "field1";
#NoLog
public String field2 = "field2";
public static void main(String[] args) throws JsonProcessingException {
final ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setAnnotationIntrospector(new NologIntrospector());
final MyObject1 myObject1 = new MyObject1();
System.out.println(objectMapper.writeValueAsString(myObject1));
}
}
I get
{"field1":"field1"}
Field2 is correctly ignored. But what I really want is
{"field1":"field1", "field2": "<***redacted***>"}
I have another annotation, #MaskSensitiveData
#Target({ElementType.FIELD, ElementType.PARAMETER})
#Retention(RetentionPolicy.RUNTIME)
#JacksonAnnotationsInside
#JsonSerialize(using = MaskSensitiveDataSerializer.class)
public #interface MaskSensitiveData {
}
public class MaskSensitiveDataSerializer extends StdSerializer<Object> {
protected MaskSensitiveDataSerializer() {
this(null);
}
public MaskSensitiveDataSerializer(Class<Object> t) {
super(t);
}
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
if (!(value instanceof String)) {
throw new RuntimeException("MaskSensitiveData annotation is only valid for string");
}
gen.writeString("<*** redacted ***>");
}
}
So what I want to do is combine them. So if a field has the special annotation and I am using my introspector, only then, do I want to redact the field. Otherwise, the field should be serialized normally.

Instead of overriding method hasIgnoreMarker in NologIntrospector. You need to override findSerializer.
This way when your introspector is enabled the custom serializer will be used else it will use the original serializer.
public class NologIntrospector extends JacksonAnnotationIntrospector {
#Override
public Object findSerializer(Annotated ann){
if (ann.hasAnnotation(NoLog.class)) {
return MaskSensitiveDataSerializer.class;
}
return super.findSerializer(ann);
}
}

Related

Deserialize multiple json fields into single java property

I want to convert a json into Java class by having custom deserializer.
I'm able to serialize ACC_NUM, NAME and any other fields from json but not sure what can be done to convert MOBILE_NUMBER_1,MOBILE_NUMBER_2 like fields into single JSONArray(See AccountInfo class). There can be many more fields like this and count also is not fixed. Example there can be ADDRESS_1, ADDRESS_2 till ADDRESS_20 and so on and all this fields should go in JSONArray of ADDRESS after deserilization.
I have a Map of Map which holds info like this:
{
"accountInfo": {
"ACC_NUM": "1234567890",
"NAME": "John Cena",
"MOBILE_NUMBER_1": "12376534",
"MOBILE_NUMBER_2": "12376534",
"MOBILE_NUMBER_3": "12376534",
"MOBILE_NUMBER_4": "12376534"
},
"someOther": {
//similer to above
}
}
This info I want to convert to the following class CommonInfo:
public class CommonInfo {
private AccountInfo accountInfo;
//other properties...
}
#JsonIgnoreProperties(ignoreUnknown = true)
public class AccountInfo {
#JsonProperty("ACC_NUM")
private FieldValue<BigInteger> accountNum;
#JsonProperty("NAME")
private FieldValue<String> name;
#JsonProperty("MOBILE_NUMBER")
private FieldValue<JSONArray> mobileNumber;
}
//FieldValue class
public interface FieldValue<T> {
T getInitialValue();
void setInitialValue(T initialValue);
T getValue();
void setValue(T value);
}
#JsonInclude(JsonInclude.Include.ALWAYS)
public class FieldValueImpl<T> implements FieldValue<T> {
protected T initialValue;
protected T value;
//getters, setters, cons..
}
My service code takes json/Map and tries to convert it to CommonInfo class
#Service
public class MyService {
private final ObjectMapper jsonMapper = new ObjectMapper();
#PostConstruct
protected void init() {
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(FieldValue.class, new FieldValueSerializer());
simpleModule.addDeserializer(FieldValue.class, new FieldValueDeserializer());
jsonMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
jsonMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
jsonMapper.registerModule(simpleModule);
}
public CommonInfo setPojoResult(Map<String, LinkedHashMap<String, String>> contentAsMap) {
return jsonMapper.convertValue(contentAsMap, CommonInfo.class);
}
}
Serializer and Deserializer looks like this:
public class FieldValueDeserializer extends JsonDeserializer<FieldValue<?>> implements ContextualDeserializer {
private JavaType valueType;
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
throws JsonMappingException {
var deserializer = new FieldValueDeserializer();
if (property == null) {
deserializer.valueType = ctxt.getContextualType().containedType(0);
} else {
var wrapperType = property.getType();
var valueType = wrapperType.containedType(0);
deserializer.valueType = valueType;
}
return deserializer;
}
#Override
public FieldValue<?> deserialize(JsonParser parser, DeserializationContext context) throws IOException {
FieldValueDeserializer deserializer = new FieldValueDeserializer();
deserializer.getKnownPropertyNames();
FieldValue<?> fieldValueImpl = new FieldValueImpl<>();
if (valueType.toString().contains("java.time.LocalDate")) {
JsonNode node = parser.getCodec().readTree(parser);
FieldValue<LocalDate> f1 = new FieldValueImpl<>();
f1.setValue(DateUtils.convertJulianToLocalDate(node.textValue()));
return f1;
} else {
fieldValueImpl.setValue(context.readValue(parser, valueType));
}
return fieldValueImpl;
}
}
//--
public class FieldValueSerializer extends StdSerializer<FieldValue> {
public FieldValueSerializer() {
this(null);
}
public FieldValueSerializer(Class<FieldValue> vc) {
super(vc);
}
#Override
public void serialize(FieldValue value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeString(String.valueOf(value.getCurValue()));
}
}

Jackson enum deserialization delegation

Is there a way to deserialize an enum which works for both the name and the object notation. I do want to keep the Shape as object for the deserialization though
e.g. This works for "type": {"name":"MYENUM"}, but what would I need to add to have it also work for "type": "MYENUM"
#JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum MyType {
#JsonProperty("MYENUM")
MYENUM("MyEnum")
public final String name = name();
MyType(String value) {
this.value = value;
}
#JsonCreator
public static MyType deserialize (#JsonProperty("name") String name) {
return MyType.valueOf(name);
}
}
Have tried adding a delegate like this
#JsonCreator(mode=JsonCreator.Mode.DELEGATING)
public static MyType deserializeString (String name) {
return MyType.valueOf(name);
}
One way to solve your problem is a custom deserializer extending StdDeserializer and inside of it check if your json file is in the form of "type": {"name":"MYENUM"} or {"type": "MYENUM"}. This checking can be obtained with the JsonNode#isObject method :
public class MyTypeDeserializer extends StdDeserializer<MyType> {
public MyTypeDeserializer() {
this(null);
}
public MyTypeDeserializer(Class<?> vc) {
super(vc);
}
#Override
public MyType deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
JsonNode root = jp.getCodec().readTree(jp);
JsonNode nodeType = root.get("type");
String name = nodeType.isObject() ? nodeType.get("name").asText() : nodeType.asText();
return MyType.valueOf(name);
}
}
Then you can annotate your MyType enum with the #JsonDeserialize(using = MyTypeDeserializer.class) deleting your deserialize method like below :
#JsonDeserialize(using = MyTypeDeserializer.class)
#JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum MyType { //omitted fields and methods for brevity }

Create custom annotation that uses #JsonIgnore based on value in environment variable in Java

I need to create a new annotation which is used to ignore a field in the output JSON file when the environment variable var == false. I tried to use JsonAnnotationIntrospector, but could not get the expected output.
public class Vehicle {
String vehicle_name;
String vehicle_model;
//getters and setters
#MyAnnotation
public String getVehicle_model() {
return vehicle_model;
}
}
Here I need to remove vehicle_model attribute when the environmental variable var == false.
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.METHOD})
#JsonIgnore
public #interface MyAnnotation {
}
This is the declaration of my custom Annotation.
Can Someone tell me how should I write the Introspector part to get the functionality I need?
Thanks in advance.
Edit: My attempt at JacksonAnnotationIntrospector
public class MyAnnotationIntrospector extends JacksonAnnotationIntrospector {
#Override
public boolean hasIgnoreMarker(AnnotatedMember annotatedMember) {
//need this part
}
}
And the implementation of ObjectMapper is
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(this);
like this
#Retention(RetentionPolicy.RUNTIME)
#Target({ElementType.FIELD, ElementType.METHOD})
public #interface MyAnnotation {
}
public class Vehicle {
private String vehicle_name;
#MyAnnotation
private String vehicle_model;
//getters and setters
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
objectMapper.setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
#Override
public boolean hasIgnoreMarker(AnnotatedMember m) {
if (!System.getenv("var").equals("true")) {
return false;
}
if(_findAnnotation(m, MyAnnotation.class) != null){
return true;
} else {
return false;
}
});
Vehicle vehicle = new Vehicle();
vehicle.setVehicle_model("vehicle_model_value");
vehicle.setVehicle_name("vehicle_name_value");
String value = objectMapper.writeValueAsString(vehicle);
System.out.println(value);
}
}

Custom serializer for serializing a List<User> in List<String>

I've a Model object Group
public class Group {
String title;
List<User> members;
String createdBy;
}
I'm using Jackson to serialize this Object. Instead of serializing the whole User object in list "members" I want to serializer just the user.getTitle() field.
Basically I want a HashMap to be something like
{
"title" : "sometitle"
"members" : [user1.getTitle(), user2.getTitle()]
}
I've written a custom serializer for this
public class GroupSerializer extends JsonSerializer<Circle> {
#Override
public void serialize(Group value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
if(value != null) {
gen.writeStartObject();
gen.writeStringField("title", value.getTitle());
gen.writeStringField("createdBy", value.getCreatedBy());
gen.writeFieldName("members");
gen.writeStartArray();
for(User user : value.getMembers()) {
gen.writeString(user.getEmail());
}
gen.writeEndArray();
gen.writeEndObject()
}
}
}
But it's not working. How do I serialize only a field of List instead of whole User Object?
I suggest that you look into Jackson's Converter interface, which seems more suited to the task than creating a custom serializer.
One approach it to create a Converter instance and add it to the ObjectMapper, so that it will be used for the serialization of all User instances.
public class UserConverter extends StdConverter<User, String> {
#Override
public String convert(User user) {
return user.getTitle();
}
}
Register it on your ObjectMapper like this:
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(User.class, new StdDelegatingSerializer(new UserConverter()));
ObjectMapper om = new ObjectMapper().registerModule(simpleModule);
Another approach, in case you don't want to convert all User instances to String, is to annotate selected properties with a converter like this:
public class Group {
String title;
#JsonSerialize(converter = ListUserConverter.class)
List<User> members;
String createdBy;
}
And have a corresponding converter that looks something like this:
public class ListUserConverter extends StdConverter<List<User>, List<String>> {
#Override
public List<String> convert(List<User> users) {
return users.stream().map(User::getTitle).collect(Collectors.toList());
}
}
Try like below :
Group:
#JsonIgnoreProperties(ignoreUnknown=true)
public class Group {
#JsonSerialize(using= TitleSerializer.class)
List<User> members;
//getters and setters
}
User:
public class User {
private String title;
//getters and setters
}
Custom Serializer :
public class TitleSerializer extends StdSerializer<List<User>> {
private static List<User> users=new ArrayList<User>();
protected TitleSerializer(Class<List<User>> t) {
super(t);
// TODO Auto-generated constructor stub
}
#SuppressWarnings("unchecked")
public TitleSerializer(){
this((Class<List<User>>) users.getClass());
}
#Override
public void serialize(List<User> users, JsonGenerator paramJsonGenerator,
SerializerProvider paramSerializerProvider) throws IOException {
paramJsonGenerator.writeStartObject();
List<String> titles=new ArrayList<String>(users.size());
for(User user: users){
titles.add(user.getTitle());
}
paramJsonGenerator.writeObjectField("members", titles);
paramJsonGenerator.writeEndObject();
}
}
Test :
Group group=new Group(Arrays.asList(new User("a"),new User("b"),new User("c")));
ObjectMapper mapper = new ObjectMapper();
String serialized = mapper.writeValueAsString(group);
System.out.println("output "+serialized);
Output:
{"members":["a","b","c"]}

Jackson cannot deserialize enum as object even if I add customized deserializer

I want to use Jackson JSON to serialize/deserialize a class containing an enum object. My class is:
class Bar {
#JsonProperty("rateType")
#JsonDeserialize(using = ReturnedRateTypeDeserializer.class)
private ReturnedRateType rateType;
public ReturnedRateType getRateType() {
return rateType;
}
public void setRateType(ReturnedRateType rateType) {
this.rateType = rateType;
}
}
The enum class ReturnedRateType is defined as:
#JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum ReturnedRateType {
AA("AA"),
BB("BB"),
CC("CC");
#JsonProperty("value")
private String value;
ReturnedRateType(String value) {
this.value = value;
}
#JsonCreator
public static ReturnedRateType fromValue(final String value) {
if (value != null) {
for (ReturnedRateType type : ReturnedRateType.values()) {
if (value.equalsIgnoreCase(type.value)) {
return type;
}
}
}
return null;
}
}
As you see, I added #JsonFormat annotation to tell Jackson to serialize this enum as POJO, and added #JsonCreator annotation to get a static factory method from given string to enum object. Since Jackson can only serialize but can't deserialize from object representation to enum, I added the following customized deserializer for the enum ReturnedRateType:
public class ReturnedRateTypeDeserializer extends JsonDeserializer<ReturnedRateType> {
#Override
public ReturnedRateType deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ReturnedRateType type = ReturnedRateType.fromValue(jp.getValueAsString());
if(type != null)
return type;
throw new JsonMappingException("invalid value for ReturnedRateType");
}
}
But when I tested deserialization from a JSON string to enum, I got the error. The JSON string is:
{"rateType": {"value": "AA"}}
My test code is:
#Test
public void RateTypeToEnum() {
String json = "{\"rateType\": {\"value\": \"AA\"}}";
System.out.println(json);
ObjectMapper mapper = new ObjectMapper();
Bar bar = null;
try {
bar = mapper.readValue(json, Bar.class);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(bar.getRateType());
}
I expect to see the output should be AA. But jp.getValueAsString() in my customized deserializer ReturnedRateTypeDeserializer is null during the execution:
ReturnedRateType type = ReturnedRateType.fromValue(jp.getValueAsString()); //jp.getValueAsString() is null here!
Thus it returns error. So what is wrong here?
According to the Jackson 2.5.X documentation on the JsonFormat annotation the Shape.Object does not work for the enum deserialisation:
Enums: Shapes JsonFormat.Shape.STRING and JsonFormat.Shape.NUMBER can
be used to change between numeric (index) and textual (name or
toString()); but it is also possible to use JsonFormat.Shape.OBJECT
to serialize (but not deserialize).
I'd make the JsonCreator static method accept a JsonNode and read the string value from it.
Note that this would work since 2.5.X. In early versions you would need to write a custom deserialiser. Here is an example:
public class JacksonEnumObjectShape {
#JsonFormat(shape = JsonFormat.Shape.OBJECT)
#JsonDeserialize(using = ReturnedRateTypeDeserializer.class)
public enum ReturnedRateType {
AA("AA"),
BB("BB"),
CC("CC");
#JsonProperty("value")
private String value;
ReturnedRateType(String value) {
this.value = value;
}
#JsonCreator
public static ReturnedRateType fromValue(final JsonNode jsonNode) {
for (ReturnedRateType type : ReturnedRateType.values()) {
if (type.value.equals(jsonNode.get("value").asText())) {
return type;
}
}
return null;
}
}
// can be avoided since 2.5
public static class ReturnedRateTypeDeserializer extends JsonDeserializer<ReturnedRateType> {
#Override
public ReturnedRateType deserialize(
final JsonParser jp,
final DeserializationContext ctxt) throws IOException {
final JsonNode jsonNode = jp.readValueAsTree();
return ReturnedRateType.fromValue(jsonNode);
}
}
public static void main(String[] args) throws IOException {
final ObjectMapper mapper = new ObjectMapper();
final String json = mapper.writeValueAsString(ReturnedRateType.AA);
System.out.println(json);
System.out.println(mapper.readValue(json, ReturnedRateType.class));
}
}
Output:
{"value":"AA"}
AA

Categories