6

I use Jackson for serialization/deserialization with my Spring Boot project.

I have a DTO object with the following structure,

public class TestDTO implements Serializable {
    private static final long serialVersionUID = 1L;

    private Long id;

    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    private UUID certificateId;

    @NotNull
    private Long orgId;

    @NotNull
    private CertificateType certificateType;

    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Valid
    @NotNull
    private PublicCertificateDTO publicCertificate;

    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    @Valid
    private PrivateCertificateDTO privateCertificate;

    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    private ZonedDateTime expiryDate;

    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    private ZonedDateTime createdDate;

    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    private ZonedDateTime updatedDate;
}

Serialization of this object in my unit tests with the following method,

public static byte[] convertObjectToJsonBytes(TestDTO object)
        throws IOException {
    ObjectMapper mapper = new ObjectMapper();
    mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

    JavaTimeModule module = new JavaTimeModule();
    mapper.registerModule(module);

    return mapper.writeValueAsBytes(object);
}

causes fields with WRITE_ONLY access to get ignored (for obvious reasons). So in the serialized object I see null values for publicCertificate and privateCertificate.

I did try setting mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)

Is there any other way to ignore these properties for Unit Tests ?

nixgadget
  • 6,983
  • 16
  • 70
  • 103

5 Answers5

5

While the solution specified works, it is an overkill for the requirement. You don't need custom serializers if all you want is to override annotations. Jackson has a mixin feature for such trivial requirements

Consider the following simplified POJO:

public class TestDTO
{
    public String regularAccessProperty;
    @JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
    public String writeAccessProperty;
}

If you want to override the @JsonProperty annotation, you create another POJO that has a variable with the exact same name (or same getter/setter names):

// mixin class that overrides json access annotation
public class UnitTestDTO
{
    @JsonProperty(access = JsonProperty.Access.READ_WRITE)
    public String writeAccessProperty;
}

You associate the original POJO and the mixin via a Simplemodule:

simpleModule.setMixInAnnotation(TestDTO.class, UnitTestDTO.class);
Sharon Ben Asher
  • 13,849
  • 5
  • 33
  • 47
3

Is there any other way to ignore these properties for Unit Tests ?

Solution: In your convertObjectToJsonBytes method, you can use:

mapper.disable(MapperFeature.USE_ANNOTATIONS);

Reference: MapperFeature.USE_ANNOTATIONS

/**
 * Feature that determines whether annotation introspection
 * is used for configuration; if enabled, configured
 * {@link AnnotationIntrospector} will be used: if disabled,
 * no annotations are considered.
 *<p>
 * Feature is enabled by default.
 */
USE_ANNOTATIONS(true),

Note: This will disable all annotations for given ObjectMapper.

Tomerikoo
  • 18,379
  • 16
  • 47
  • 61
100rabh
  • 142
  • 9
1

Another solution is to override the annotation inspector with a simple custom class. That would be the minimal example:

ObjectMapper mapper = new ObjectMapper().setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
     @Override
     public JsonProperty.Access findPropertyAccess(Annotated m) {
           return null;
     }
});

Other solution for Spring Boot @Autowired object mappers:

  1. Use a dedicated class so it's reusable and more readable:
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;

public class IgnoreReadOnlyFieldsAnnotationInspector extends JacksonAnnotationIntrospector {
    @Override
    public JsonProperty.Access findPropertyAccess(Annotated m) {
        return null;
    }
}

  1. Within the test use @BeforeEach (or her older friends)
public class AmazingTest {
  @Autowired
  ObjectMapper mapper;

 @BeforeEach
    void beforeAll(){
        // need to copy because the autowired mapper in test and the object mapper in code under test are the same instance
        mapper = objectMapper.copy();
        mapper.setAnnotationIntrospector(new IgnoreReadOnlyFieldsAnnotationInspector());
    }
}
Tim Malich
  • 1,301
  • 14
  • 22
0

This was solved by adding a custom serializer for the JUnit tests.

So for TestDTO I added the serializer as below.

private class TestJsonSerializer extends StdSerializer<TestDTO> {
    public TestJsonSerializer() {
        this(null);
    }

    public TestJsonSerializer(Class<TestDTO> t) {
        super(t);
    }

    @Override
    public void serialize(TestDTO value, JsonGenerator gen, SerializerProvider provider) throws IOException {
        gen.writeStartObject();
        gen.writeNumberField("orgId", value.getOrgId());
        gen.writeStringField("certificateType", value.getCertificateType().getType());
        if (value.getPublicCertificate() != null) {
            gen.writeObjectField("publicCertificate", value.getPublicCertificate());
        }
        if (value.getPrivateCertificate() != null) {
            gen.writeObjectField("privateCertificate", value.getPrivateCertificate());
        }
        gen.writeObjectField("expiryDate", value.getExpiryDate());
        gen.writeObjectField("createdDate", value.getCreatedDate());
        gen.writeObjectField("updatedDate", value.getUpdatedDate());
        gen.writeEndObject();
    }
}

I then added,

ObjectMapper mapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(TestDTO.class, new TestJsonSerializer());
mapper.registerModule(simpleModule);

Similarly added and registered custom serializers for nested objects, publicCertificate and privateCertificate.

nixgadget
  • 6,983
  • 16
  • 70
  • 103
-1

Here a simple example

@ToString
@Getter
@Setter
public class Account implements Cloneable {

    @JsonProperty(access = Access.WRITE_ONLY)
    private Integer accountId;
    private String accountType;
    private Long balance;

public AccountTest clone() {
    AccountTest test = new AccountTest();
    test.setAccountId(this.accountId);
    test.setAccountType(this.accountType);
    test.setBalance(this.balance);
    return test;
}

}

@ToString
@Getter
@Setter
public class AccountTest {

    private Integer accountId;
    private String accountType;
    private Long balance;
}

    public static void main(String[] args) {
              ObjectMapper mapper = new ObjectMapper();
    try {
        Account account = new Account();
        account.setAccountId(1999900);
        account.setAccountType("Saving");
        account.setBalance(2433l);
        AccountTest accountTest = account.clone();
        System.out.println(account);

        byte[] accountBytes = mapper.writeValueAsBytes(account);
        System.out.println(new String(accountBytes));

        byte[] accountTestBytes = mapper.writeValueAsBytes(accountTest);
        System.out.println(new String(accountTestBytes));
    } catch (IOException e) { }

    }

}

Yoga Gowda
  • 357
  • 4
  • 8
  • please re-read the question, with the example code. the question is about *serialization* not de-serialization – Sharon Ben Asher Jun 08 '20 at 18:13
  • Hey @Sharon, I missed that, still we can achieve Serialization without mixin, take a look at updated sample code, the clone method added just to ease for testing. The clone is not required. – Yoga Gowda Jun 14 '20 at 03:39
  • so this solution requires creating an identical class with *all* properties of the original and also copying them, which requires extra development (clone might not always be sufficient) as well as have a run time performance impact. my solution requires creating a class with only the properties that require change in json annotation. that's it, no cloning or copying. only the "cost" of the json annotation. at the very least there are pros and cons to each solution. – Sharon Ben Asher Jun 14 '20 at 05:14
  • now, imagine a class with 200 properties. in my solution, it is immediately clear what is the difference that requires the mixin. in your solution, it is extremely difficult for the passing by dev to gather where are the differences between the two behemoth classes. furthermore, imagine a class that is changing with every software version, adding/removing properties as product requirements change. your solution would require duplicating these changes each and every time. frankly, I don't see the advantage of your solution at all. – Sharon Ben Asher Jun 14 '20 at 05:20