dermoritz
dermoritz

Reputation: 13011

Jackson how map one Pojo field to 2 (json) fields (same content, different name)

I use Jackson to serialise POJOs into CSV. Now we need to change the naming for certain fields to snake_case. This is easily done by @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class).

For compatibility reasons we need some of the renamed fields also with their old name.

E.g.:

public class Pojo {
    private int someField;
}

Default will serialise to "someField", SnakeCaseStrategy will serialise to "some_field".

How to get serialization with both?:

{
  "someField" : "one",
  "some_field" : "one" 
}

My first try was a mixin:

public abstract class PojoFormat {

    @JsonProperty("someField")
    abstract String getSomeField();

}

but this effectively only undoes the naming strategy change. So how to copy a field in serialization - preferable not by changing the Pojo (this copied fields should be removed when all clients can cope with it).

Little update:

in my real class there some nested class that use JsonUnwrapped and the doc stated that this is not working with custom serializer (didn't know that this makes a difference here).

Upvotes: 4

Views: 1686

Answers (2)

Michał Ziober
Michał Ziober

Reputation: 38655

You can try to use JsonAnyGetter annotation and define for every POJO extra mapping for backward compatibility.

Let's create a simple interface:

interface CompatibleToVer1 {

    @JsonAnyGetter
    Map<String, Object> getCompatibilityView();
}

and two classes which implement it:

@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
class RootPojo implements CompatibleToVer1 {
    private int rootId;

    @JsonUnwrapped
    private SomePojo pojo;

    @Override
    public Map<String, Object> getCompatibilityView() {
        return Collections.singletonMap("rootId", rootId);
    }
}

@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
class SomePojo implements CompatibleToVer1 {

    private int someField;
    private String someName;

    @Override
    public Map<String, Object> getCompatibilityView() {
        Map<String, Object> extra = new LinkedHashMap<>();
        extra.put("someField", someField);

        return extra;
    }
}

As you can see, I defined extra columns for each POJO with custom names. Serialising to JSON is straightforward:

ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);

SomePojo pojo = new SomePojo(123, "Tom");
mapper.writeValue(System.out, new RootPojo(1, pojo));

Above code prints:

{
  "root_id" : 1,
  "some_field" : 123,
  "some_name" : "Tom",
  "someField" : 123,
  "rootId" : 1
}

But for CSV we need to create extra configuration:

CsvMapper csvMapper = CsvMapper.builder().build();

CsvSchema pojoExtraScheme = CsvSchema.builder()
        .addColumn("someField")
        .build();
CsvSchema rootExtraScheme = CsvSchema.builder()
        .addColumn("rootId")
        .build();
CsvSchema compatibleSchema = CsvSchema.emptySchema()
        .withHeader()
        .withColumnsFrom(csvMapper.schemaFor(RootPojo.class))
        .withColumnsFrom(rootExtraScheme)
        .withColumnsFrom(csvMapper.schemaFor(SomePojo.class))
        .withColumnsFrom(pojoExtraScheme);


SomePojo tom = new SomePojo(123, "Tom");
SomePojo jerry = new SomePojo(124, "Jerry");
List<RootPojo> pojos = Arrays.asList(new RootPojo(1, tom), new RootPojo(2, jerry));
ObjectWriter writer = csvMapper.writer(compatibleSchema);
System.out.println(writer.writeValueAsString(pojos));

Above code prints:

some_field,some_name,root_id,rootId,someField
123,Tom,1,1,123
124,Jerry,2,2,124

If you do not want to specify extra columns two times you can implement builder method based on our interface:

CsvSchema createSchemaFor(CompatibleToVer1 entity) {
    CsvSchema.Builder builder = CsvSchema.builder();
    entity.getCompatibilityView().keySet().forEach(builder::addColumn);

    return builder.build();
}

and use as below:

CsvSchema compatibleSchema = CsvSchema.emptySchema()
        .withHeader()
        .withColumnsFrom(csvMapper.schemaFor(RootPojo.class))
        .withColumnsFrom(createSchemaFor(new RootPojo()))
        .withColumnsFrom(csvMapper.schemaFor(SomePojo.class))
        .withColumnsFrom(createSchemaFor(new SomePojo()));

Using JsonAnyGetter with CSV is really tricky and could be problematic mixing it with other annotations, take a look at: Could please add JsonAnyGetter and JsonAnySetter annotations support?

Upvotes: 1

sgtcortez
sgtcortez

Reputation: 427

Well, I have never seen this before, I would be very happy if someone here in this site knows how.

The easy way, in my opinion, is to use a Custom Serializer.

For example:

  1. Using the @JsonSerialize annotation
  2. Register a module
  3. Dynamic Serializer with Reflection

@JsonSerialize annotation

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;

@JsonSerializer(using=PojoSerializer.class)
class Pojo {
 
  private String myValue;
   
  // getters and setters

}

class PojoSerializer extends StdSerializer<Pojo> {

  public PojoSerializer() {
   super(Pojo.class);
  }

      @Override
    public void serialize(Pojo value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        gen.writeStartObject();
        gen.writeStringField("myValue", value.getMyValue());
        gen.writeStringField("my_value", value.getMyValue());
        gen.writeEndObject();

    }
}

Module

static class Pojo {

    private String myValue;

    public String getMyValue() {
        return myValue;
    }

    public Pojo setMyValue(String myValue) {
        this.myValue = myValue;
        return this;
    }
}

static class PojoSerializer extends StdSerializer<Pojo> {

    public PojoSerializer() {
        super(Pojo.class);
    }

    @Override
    public void serialize(Pojo value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        gen.writeStartObject();
        gen.writeStringField("myValue", value.getMyValue());
        gen.writeStringField("my_value", value.getMyValue());
        gen.writeEndObject();
    }
}


public static void main(String[] args) throws  JsonProcessingException {

    final ObjectMapper mapper = new ObjectMapper();

    final SimpleModule module = new SimpleModule("PojoModule");

    module.addSerializer(Pojo.class, new PojoSerializer());

    mapper.registerModule(module);

    final Pojo pojo = new Pojo();
    pojo.setMyValue("This is the value of my pojo");

    System.out.println(mapper.writeValueAsString(pojo));

}

Reflection

I write some code for you, you might want to see to get new ideias. This works as a generic way(just to not write several serializers).

// The serializer will be register in the ObjectMapper module.
static class Pojo {

    private String myValue = "With snake and camel";
    private String value = "Without snake case";
    private String thirdValue = "snake & camel";

}

// using the annotation
@JsonSerialize(using = PojoSerializer.class)
static class Pojo2 {

    private String pojoName = "Pojo 2";
    private String pojo = "pojp";

}

static class PojoSerializer extends StdSerializer<Object> {

    public PojoSerializer() {
        super(Object.class);
    }

    @Override
    public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        gen.writeStartObject();

        final Field[] fields = value.getClass().getDeclaredFields();
        for(final Field field : fields) {

            final String name = field.getName();
            final String fieldValue;
            try {
                // Do not use this!
                fieldValue = (String)field.get(value);
            } catch (IllegalAccessException e) {
                throw new RuntimeException(e);
            }

            byte firstUpperCase = -1;
            for(byte index = 0; index < name.length(); index++) {
                final char caractere = name.charAt(index);

                // A ascii code is 66 decimal, and 90 is the Z in decimal
                if(caractere > 'A' && caractere < 'Z') {
                    // found the first upper
                    firstUpperCase = index;
                    break;
                }
            }

            // writes the normal field name
            gen.writeStringField(name, fieldValue);

            // if the name is in camel case, we will write in snake case too.
            if(firstUpperCase != -1) {
                final char lowerLetter = (char)((int) name.charAt(firstUpperCase) + 32);
                final String left = name.substring(0, firstUpperCase);
                final String right = String.format("%c%s",lowerLetter, name.substring(firstUpperCase + 1));
                gen.writeStringField(String.format("%s_%s", left, right), fieldValue);

            }
        }
        gen.writeEndObject();
    }
}

Upvotes: 2

Related Questions