How can I write a JUnit-Test for custom Jackson Serializer? - java

I wanted to test my serializer which parses my java object to a json object. This is my Serializer class:
public class CountryCodeSerializer extends JsonSerializer<CountryCode> {
#Override
public void serialize(CountryCode value, JsonGenerator generator, SerializerProvider provider)
throws IOException, JsonProcessingException {
if (value == null) {
generator.writeString("{}");
} else {
generator.writeString(value.toString());
}
}
}
My test looks like this:
#Before
public void setUp() throws Exception {
stringJson = new StringWriter();
generator = new JsonFactory().createGenerator(stringJson);
provider = new ObjectMapper().getSerializerProvider();
countryCode = CountryCode.parse("us");
}
#Test
public void parsingNullReturnsNull() throws Exception {
assertThat(countryCodeSerializer.serialize(countryCode, generator, provider)).isEqualTo("{'countrycode':'us'}); //this doesn't work, since serialize() is void
//countryCodeSerializer.serialize(countryCode, generator, provider); //this throws an java.lang.NullPointerException
}
So how can I test my serializer? I tried other answers to similar questions, but nothing worked for me.
I use the serializer like this in my other clases:
#JsonSerialize(using = CountryCodeSerializer.class)
private CountryCode countryCode;

Ok thank you for your answers. I got it now this way and it works fine:
I changed my serializer a little bit:
public class CountryCodeSerializer extends JsonSerializer<CountryCode> {
#Override
public void serialize(CountryCode value, JsonGenerator generator, SerializerProvider provider)
throws IOException, JsonProcessingException {
if (null == value) {
throw new IllegalArgumentException("CountryCode is null");
} else {
generator.writeString(value.toString());
}
}
}
And here are my two tests:
public class CountryCodeSerializerTest {
private CountryCodeSerializer countryCodeSerializer;
private JsonGenerator jsonGenerator;
#Before
public void setUp() throws Exception {
countryCodeSerializer = new CountryCodeSerializer();
jsonGenerator = mock(JsonGenerator.class);
}
#Test
public void testNullCountryCodeThrowsIllegalArgumentException() throws Exception {
try {
countryCodeSerializer.serialize(null, jsonGenerator, null);
fail("An IllegalArgumentException should have been thrown.");
} catch (IllegalArgumentException e) {
//ok
}
}
#Test
public void testCountryCodeConvertedToJsonString() throws Exception {
countryCodeSerializer.serialize(CountryCode.parse("us"), jsonGenerator, null);
verify(jsonGenerator).writeString("us");
}
}

Something like this:
#Mock
private JsonGenerator generator;
#Test
public void testInstanceWithValue() {
//SETUP
String expectedValue = "test value";
CountryCode value = mock(CountryCode.class);
when(value.toString()).thenReturn(expectedValue);
// CALL
CountryCodeSerializer instance = new CountryCodeSerializer(value, generator, null);
// VERIFY
verify(generator).writeString(expectedValue);
}
#Test
public void testInstanceWithNull() {
//SETUP
CountryCode value = null;
// CALL
CountryCodeSerializer instance = new CountryCodeSerializer(value, generator, null);
// VERIFY
verify(generator).writeString("{}");
}

This can be achieved by creating a custom JsonGenerator that stores what is written to it.
class TestJsonGenerator extends JsonGenerator {
private StringBuilder stringBuilder = new StringBuilder();
...
#Override
public void writeString(String text) {
stringBuilder.append(text);
}
public String getText() {
return stringBuilder.toString();
}
}
Then you verify the generated content, without needing to check all the calls to writeString that were made:
TestJsonGenerator testGenerator = new TestJsonGenerator();
serializer.serialize(countryCode, testGenerator, provider);
assertThat(testGenerator.getText()).isEqualsTo("{ \"foo\": \"bar\" }");

Related

How to prepare or mock JsonParser to test custom StdDeserializer

I have custom StdDeserializer<Date>, how can i unit test the overridden deserialize method here?
or how can i prepare or mock JsonParser here for unit testing desterilize method?
public class StringToDateDeserializer extends StdDeserializer<Date> {
protected StdDateFormat df = new StdDateFormat();
public StringToDateDeserializer() {
this(null);
}
protected StringToDateDeserializer(Class<?> T) {
super(T);
}
#Override
public Date deserialize(JsonParser jsonParser, DeserializationContext ctxt) throws IOException {
String dateStr = jsonParser.getText();
if (StringUtils.isEmpty(dateStr)) {
return null;
}
try {
return df.parse(dateStr);
} catch (ParseException e) {
throw new MyCustomException("Invalid date passed, ISO 8601 is expected");
}
}
}
Example of test for StringToDateDeserializer with 100% coverage.
public class TestClass {
private ObjectMapper mapper;
private StringToDateDeserializer deserializer;
#Before
public void setup() {
mapper = new ObjectMapper();
deserializer = new StringToDateDeserializer();
}
#Test
public void dateTest() throws IOException {
Date date = deserializer.deserialize(prepareParser("{ \"value\":\"2020-07-10T15:00:00.000\" }"), mapper.getDeserializationContext());
Assert.assertNotNull(date);
Assert.assertEquals(1594393200000L, date.getTime());
}
#Test(expected = MyCustomException.class)
public void exceptionalTest() throws IOException {
deserializer.deserialize(prepareParser("{ \"value\":\"2020-07\" }"), mapper.getDeserializationContext());
}
#Test
public void nullTest() throws IOException {
Date date = deserializer.deserialize(prepareParser("{ \"value\":\"\" }"), mapper.getDeserializationContext());
Assert.assertNull(date);
}
private JsonParser prepareParser(String json) throws IOException {
JsonParser parser = mapper.getFactory().createParser(json);
while (parser.nextToken() != JsonToken.VALUE_STRING);
return parser;
}
}

How to throw a custom exception from a custom deserializer

I have a class deserialized by my custom deserializer and I need to throw my custom exception.
public class MyClass {
}
public static void main(String[] args) {
try {
ObjectMapper mapper = new ObjectMapped();
MyClass myClass = mapper.readValue(json, MyClass.class);
} catch(Exception e) {
System.out.println(e); // JsonMappingException
System.out.println(e.getCause()); // null, but I need to get my CustomException.class
}
}
public class MyDeserializer extends StdDeserializer<MyClass> {
#Override
public MyClass deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
throw new CustomException("TestException", 1);
}
}
public class CustomException extends IOException {
private int code;
public CustomException(String message, int code) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}
What can I do to return my custom exception from the custom deserializer?
First you should add custom desializer to ObjectMapper or target Object like below
#JsonDeserialize(using = MyDeserializer.class)
public class MyClass {
}
getCause() return null because you already have caused Exception, check getCause implementation
public synchronized Throwable getCause() {
return (cause==this ? null : cause);
}
shorty you don't need to getCause, e is your CustomException instance. Just do first step and remove getCause, it ll be fine.

How to alter the value of a JsonNode while constructing it from a string in Jackson

I have a JSON string and I want to alter the value while constructing the JsonNode using Jackson library.
eg:-
input: {"name":"xyz","price":"90.00"}
output:{"name":"xyz-3","price":90.90}
I created my own JsonFactory and passed my own Parser. but I can only alter the keys, not the values associated with a key.
code:
private static ObjectMapper create() {
ObjectMapper objectMapper = new ObjectMapper(new JsonFactory() {
#Override
protected JsonParser _createParser(byte[] data, int offset, int len, IOContext ctxt) throws IOException {
return new MyParser(super._createParser(data, offset, len, ctxt));
}
#Override
protected JsonParser _createParser(InputStream in, IOContext ctxt) throws IOException {
return new MyParser(super._createParser(in, ctxt));
}
#Override
protected JsonParser _createParser(Reader r, IOContext ctxt) throws IOException {
return new MyParser(super._createParser(r, ctxt));
}
#Override
protected JsonParser _createParser(char[] data, int offset, int len, IOContext ctxt, boolean recyclable)
throws IOException {
return new MyParser(super._createParser(data, offset, len, ctxt, recyclable));
}
});
private static final class MyParser extends JsonParserDelegate {
private MyParser(JsonParser d) {
super(d);
}
#Override
public String getCurrentName() throws IOException, JsonParseException {
....
}
#Override
public String getText() throws IOException, JsonParseException {
...
}
#Override
public Object getCurrentValue() {
...
}
#Override
public String getValueAsString() throws IOException {
...
}
#Override
public String getValueAsString(String defaultValue) throws IOException {
...
}
}
Below is the code to construct the JsonNode from the string.
mapper.readTree(jsonStr);
In this case when the readTree method is called the getCurrentValue or getValueAsString methods are not called, so I am not able to alter the value while creating the JsonNode itself.
Also the json strings can be different. Basically I want to construct a JsonNode from the string. so tying to a specific schema/bean is not a good choice here.
How to address this ? TIA
Adding the updated code for version 2.7.4:-
static class MyParser extends JsonParserDelegate {
MyParser(final JsonParser delegate) {
super(delegate);
}
#Override
public String getText() throws IOException {
final String text = super.getText();
if ("name".equals(getCurrentName())) {
return text + "-3";
}
return text;
}
#Override
public JsonToken nextToken() throws IOException {
if ("price".equals(getCurrentName())) {
// Advance token anyway
super.nextToken();
return JsonToken.VALUE_NUMBER_FLOAT;
}
return super.nextToken();
}
#Override
public int getCurrentTokenId() {
try {
if ("price".equals(getCurrentName())) {
return JsonTokenId.ID_NUMBER_FLOAT;
}
} catch (final IOException e) {
//
}
return super.getCurrentTokenId();
}
#Override
public NumberType getNumberType() throws IOException {
if ("price".equals(getCurrentName())) {
return NumberType.FLOAT;
}
return super.getNumberType();
}
#Override
public float getFloatValue() throws IOException {
return Float.parseFloat(getValueAsString("0")) + 0.09F;
}
#Override
public double getDoubleValue() throws IOException {
return Double.parseDouble(getValueAsString("0")) + 0.09D;
}
}
pom.xml:-
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>2.8.7</version>
<!--<scope>test</scope>-->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.8.7</version>
</dependency>
Edit: there is a subtle difference between 2.7.* and 2.9.*.
While 2.9.* is able to differentiate between double and float with
getDoubleValue()
getFloatValue()
instead 2.7.* only uses
getDoubleValue()
even for ID_NUMBER_FLOAT tokens.
So, you need to decide if you want to maintain retro-compatibility or not.
You can also override both, like I did here.
This is all what you need for your custom MyParser
static class MyParser extends JsonParserDelegate {
MyParser(final JsonParser delegate) {
super(delegate);
}
#Override
public String getText() throws IOException {
final String text = super.getText();
if ("name".equals(getCurrentName())) {
return text + "-3";
}
return text;
}
#Override
public JsonToken nextToken() throws IOException {
if ("price".equals(getCurrentName())) {
// Advance token anyway
super.nextToken();
return JsonToken.VALUE_NUMBER_FLOAT;
}
return super.nextToken();
}
#Override
public int getCurrentTokenId() {
try {
if ("price".equals(getCurrentName())) {
return JsonTokenId.ID_NUMBER_FLOAT;
}
} catch (final IOException e) {
//
}
return super.getCurrentTokenId();
}
#Override
public NumberType getNumberType() throws IOException {
if ("price".equals(getCurrentName())) {
return NumberType.FLOAT;
}
return super.getNumberType();
}
#Override
public float getFloatValue() throws IOException {
return Float.parseFloat(getValueAsString("0")) + 0.09F;
}
#Override
public double getDoubleValue() throws IOException {
return Double.parseDouble(getValueAsString("0")) + 0.09D;
}
}
Output: {"name":"xyz-3","price":90.09}
Your code seems fine, and it's tested and working ;)
Are you really sure that regarding the Separation of Concerns it is a good idea to mix parsing and changes within the parsed data?
If you still want to do this, you could use a Custom Deserializer and treat your wanted field names and types the way you want it, like:
class CustomDeserializer extends StdDeserializer<Entity> {
public CustomDeserializer(Class<Entity> t) {
super(t);
}
#Override
public Entity deserialize(JsonParser jp, DeserializationContext dc) throws IOException {
String name = null;
float price = 0;
JsonToken currentToken = null;
while ((currentToken = jp.nextValue()) != null) {
switch (currentToken) {
case VALUE_STRING:
switch (jp.getCurrentName()) {
case "name":
name = jp.getText() + "-3"; // change this text to whatever you want;
break;
case "price":
price = Float.parseFloat(jp.getText()); // parse
break;
default:
break;
}
break;
default:
break;
}
}
return new Entity(name, price);
}
}
And after registering your custom deserializer it works on any object mapper you want:
#Test
public void customDeserialization() throws IOException {
// given
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Entity.class, new CustomDeserializer(Entity.class));
mapper.registerModule(module);
// when
Entity entity = mapper.readValue("{\"name\":\"xyz\",\"price\":\"90.00\"}", Entity.class);
// then
assertThat(entity.getName()).isEqualTo("xyz-3");
assertThat(entity.getPrice()).isEqualTo(90f);
}

How to get enum class in generic method?

I am trying to create a standard way to serialize and deserialize for Enum on Jackson.
My serialize is easy:
public class EnumSerializer extends JsonSerializer<Enum<?>> {
#Override
public void serialize(Enum<?> data, JsonGenerator gen, SerializerProvider provider)
throws IOException, JsonProcessingException {
if (data == null) {
gen.writeString("");
} else {
gen.writeString(data.name());
}
}
}
Now I am working on Deserializer:
public class EnumDeserializer extends JsonDeserializer<Enum<?>> {
#Override
public Enum<?> deserialize(JsonParser jsonparser, DeserializationContext deserializationcontext)
throws IOException, JsonProcessingException {
String dataStr = jsonparser.getText();
if (dataStr == null || dataStr.isEmpty()) {
return null;
} else {
Class<Enum<?>> enumClass = null; // How can I get enumClass?
for(Enum<?> one: enumClass.getEnumConstants()){
if(one.name().equals(dataStr)){
return one;
}
}
return null;
}
}
}
But you can see I have trouble to get enumClass.
Could you please help me?
Thanks!
If you really want to create the custom EnumDeserializer you can see the implementation of Jackson:
com.fasterxml.jackson.databind.deser.std.EnumDeserializer
But as I can see you try to implement the standard behavior of Jackson.

Serialize generic field from java object to json

I've a generic field in User.java. I want to use the value of T in json.
public class User<T> {
public enum Gender {MALE, FEMALE};
private T field;
private Gender _gender;
private boolean _isVerified;
private byte[] _userImage;
public T getField() { return field; }
public boolean isVerified() { return _isVerified; }
public Gender getGender() { return _gender; }
public byte[] getUserImage() { return _userImage; }
public void setField(T f) { field = f; }
public void setVerified(boolean b) { _isVerified = b; }
public void setGender(Gender g) { _gender = g; }
public void setUserImage(byte[] b) { _userImage = b; }
}
and mapper class is:
public class App
{
public static void main( String[] args ) throws JsonParseException, JsonMappingException, IOException
{
ObjectMapper mapper = new ObjectMapper();
Name n = new Name();
n.setFirst("Harry");
n.setLast("Potter");
User<Name> user = new User<Name>();
user.setField(n);
user.setGender(Gender.MALE);
user.setVerified(false);
mapper.writeValue(new File("user1.json"), user);
}
}
and the json output is :
{"field":{"first":"Harry","last":"Potter"},"gender":"MALE","verified":false,"userImage":null}
In the output, i want Name to be appeared in place of field. How do i do that. Any help?
I think what u ask is not JSON's default behavior. Field name is the "key" of the json map, not the variable name. U should rename the field or make some String process to do it.
private T field;
change the above to this:
private T name;
You need a custom serializer to do that. That's a runtime data transformation and Jackson has no support for data transformation other than with a custom serializer (well, there's wrapping/unwrapping of value, but let's not go there). Also, you will need to know in advance every type of transformation you want to apply inside your serializer. The following works:
public class UserSerializer extends JsonSerializer<User<?>> {
private static final String USER_IMAGE_FIELD = "userImage";
private static final String VERIFIED_FIELD = "verified";
private static final String FIELD_FIELD = "field";
private static final String NAME_FIELD = "name";
#Override
public void serialize(User<?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonProcessingException {
jgen.writeStartObject();
if (value.field instanceof Name) {
jgen.writeFieldName(NAME_FIELD);
} else {
jgen.writeFieldName(FIELD_FIELD);
}
jgen.writeObject(value.field);
jgen.writeStringField("gender", value._gender.name());
jgen.writeBooleanField(VERIFIED_FIELD, value._isVerified);
if (value._userImage == null) {
jgen.writeNullField(USER_IMAGE_FIELD);
} else {
jgen.writeBinaryField(USER_IMAGE_FIELD, value._userImage);
}
jgen.writeEndObject();
}
}

Categories

Resources