I want to use Jackson to deserialize and later serialize jsons using Kotlin's data classes. It's important that I maintain the distinction between an explicitly null property, and a property which was omitted in the original json.
I have a large domain model (50+ classes) built almost entirely out of Kotlin data classes. Kotlin's data classes provide a lot of useful functionalities that I need to use elsewhere in my program, and for that reason I'd like to keep them instead of converting my models.
I've currently got this code working, but only for Java classes or using Kotlin properties declared in the body of the Kotlin class, and not working for properties declared in the constructor. For Kotlin's data class utility functions to work, all properties must be declared in the constructor.
Here's my object mapper setup code:
val objectMapper = ObjectMapper()
objectMapper.registerModule(KotlinModule())
objectMapper.registerModule(Jdk8Module())
objectMapper.setSerializationInclusion(JsonInclude.Include.ALWAYS)
objectMapper.configOverride(Optional::class.java).includeAsProperty =
JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, null)
objectMapper.configure(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES, true)
objectMapper.configure(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN, true)
objectMapper.configure(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS, true)
objectMapper.nodeFactory = JsonNodeFactory.withExactBigDecimals(true)
Here are my test classes:
TestClass1.java
public class TestClass1 {
public TestClass1() {}
public TestClass1(int intVal, Optional<Double> optDblVal) {
this.intVal = intVal;
this.optDblVal = optDblVal;
}
public Integer intVal;
public Optional<Double> optDblVal;
}
TestClasses.kt
data class TestClass2(val intVal: Int?, val optDblVal: Optional<Double>?)
class TestClass3(val intVal: Int?, val optDblVal: Optional<Double>?)
class TestClass4 {
val intVal: Int? = null
val optDblVal: Optional<Double>? = null
}
and here are my tests:
JsonReserializationTests.kt
#Test
fun `Test 1 - Explicit null Double reserialized as explicit null`() {
val inputJson = """
{
"intVal" : 7,
"optDblVal" : null
}
""".trimIndent()
val intermediateObject = handler.objectMapper.readValue(inputJson, TestClassN::class.java)
val actualJson = handler.objectMapper
.writerWithDefaultPrettyPrinter()
.writeValueAsString(intermediateObject)
.replace("\r", "")
assertEquals(inputJson, actualJson)
}
#Test
fun `Test 2 - Missing Double not reserialized`() {
val inputJson = """
{
"intVal" : 7
}
""".trimIndent()
val intermediateObject = handler.objectMapper.readValue(inputJson, TestClassN::class.java)
val actualJson = handler.objectMapper
.writerWithDefaultPrettyPrinter()
.writeValueAsString(intermediateObject)
.replace("\r", "")
assertEquals(inputJson, actualJson)
}
Test Results for each class
Let's talk about TestClass2.
If you convert Kotlin code to Java Code, you can find the reason.
Intellij offers a converting tool for Kotlin. You can find it from the menu Tools -> Kotlin -> Show Kotlin Bytecode.
Here is a Java code from the TestClass2 Kotlin code.
public final class TestClass2 {
#Nullable
private final Integer intVal;
#Nullable
private final Optional optDblVal;
#Nullable
public final Integer getIntVal() {
return this.intVal;
}
#Nullable
public final Optional getOptDblVal() {
return this.optDblVal;
}
public TestClass2(#Nullable Integer intVal, #Nullable Optional optDblVal) {
this.intVal = intVal;
this.optDblVal = optDblVal;
}
#Nullable
public final Integer component1() {
return this.intVal;
}
#Nullable
public final Optional component2() {
return this.optDblVal;
}
#NotNull
public final TestClass2 copy(#Nullable Integer intVal, #Nullable Optional optDblVal) {
return new TestClass2(intVal, optDblVal);
}
// $FF: synthetic method
public static TestClass2 copy$default(TestClass2 var0, Integer var1, Optional var2, int var3, Object var4) {
if ((var3 & 1) != 0) {
var1 = var0.intVal;
}
if ((var3 & 2) != 0) {
var2 = var0.optDblVal;
}
return var0.copy(var1, var2);
}
#NotNull
public String toString() {
return "TestClass2(intVal=" + this.intVal + ", optDblVal=" + this.optDblVal + ")";
}
public int hashCode() {
Integer var10000 = this.intVal;
int var1 = (var10000 != null ? var10000.hashCode() : 0) * 31;
Optional var10001 = this.optDblVal;
return var1 + (var10001 != null ? var10001.hashCode() : 0);
}
public boolean equals(#Nullable Object var1) {
if (this != var1) {
if (var1 instanceof TestClass2) {
TestClass2 var2 = (TestClass2)var1;
if (Intrinsics.areEqual(this.intVal, var2.intVal) && Intrinsics.areEqual(this.optDblVal, var2.optDblVal)) {
return true;
}
}
return false;
} else {
return true;
}
}
The original code is too long, so here is the constructor only.
public TestClass2(#Nullable Integer intVal, #Nullable Optional optDblVal) {
this.intVal = intVal;
this.optDblVal = optDblVal;
}
Since Jackson library cannot create an instance without parameters because there is no non-parameter constructor, it will try to create a new instance with some parameters. For test case 2, JSON has only one parameter so that it will look for a one-parameter constructor, but there is no so that it will throw an exception. This is also why test case 1 is passed.
Therefore, what you have to do is that you have to give all default values to all the parameters of data class to make a non-parameter constructor like the below code.
data class TestClass2(val intVal: Int? = null, val optDblVal: Optional<Double>? = null)
Then, if you see in Java code, the class will have a non-parameter constructor.
public TestClass2(#Nullable Integer intVal, #Nullable Optional optDblVal) {
this.intVal = intVal;
this.optDblVal = optDblVal;
}
// $FF: synthetic method
public TestClass2(Integer var1, Optional var2, int var3, DefaultConstructorMarker var4)
{
if ((var3 & 1) != 0) {
var1 = (Integer)null;
}
if ((var3 & 2) != 0) {
var2 = (Optional)null;
}
this(var1, var2);
}
public TestClass2() {
this((Integer)null, (Optional)null, 3, (DefaultConstructorMarker)null);
}
So, if you still want to use Kotlin data class, you have to give default values to all the variables.
data class Example(
val greeting: Optional<String>? = null
)
This allows you to distinguish all three cases in the JSON:
non-null value ({"greeting":"hello"} → greeting.isPresent() == true)
null value ({"greeting":null} → greeting.isPresent() == false)
not present ({ } → greeting == null)
This is just a concise summary of what #Pemassi offered, with the key insight being to make a default null assignment to the nullable Optional<T> member.
Note that the semantics of .isPresent() is potentially confusing to a casual observer, since it does not refer to the presence of a value in the JSON.
A full unit test demonstration is here.
I just found the Kotiln Plugin that makes no-argument constructor for data class automatically. This should help you without much editing. However, this is not a good design pattern, so I still recommend giving default value to all members.
Here is a link for the Kotlin NoArg Plugin
https://kotlinlang.org/docs/reference/compiler-plugins.html#no-arg-compiler-plugin
Related
Let's say I have a data class like this:
data class MyData(val something: Int, val somethingElse : String) {
init {
require(something > 20) { "Something must be > 20" }
require(StringUtils.isNotEmtpy(somethingElse)) { "Something else cannot be blank" }
}
}
I'd like to be able to apply a function to somethingElse before the init method is called. In this case I want to remove all \n characters from the somethingElse String while maintaining immutability of the field (i.e. somethingElse must still be a val). I'd like to do something similar to this in Java:
public class MyData {
private final int something;
private final String somethingElse;
public MyDate(int something, String somethingElse) {
this.something = something;
this.somethingElse = StringUtils.replace(somethingElse, '\n', '');
Validate.isTrue(something > 20, "...");
Validate.isTrue(StringUtils.isNotEmtpy(this.somethingElse), "...");
}
// Getters
}
I could of course create a normal class (i.e. no data class) in Kotlin but I want MyData to be a data class.
What is the idiomatic way to do this in Kotlin?
While you can not literally do what you want, you can fake it.
Make all constructors of your data class private.
Implement factories/builders/whatevers on the companion as operator fun invoke.
Usages of Companion.invoke will -- in Kotlin! -- look just like constructor calls.
In your example:
data class MyData private constructor(
val something: Int,
val somethingElse : String
) {
init {
require(something > 20) { "Something must be > 20" }
require("" != somethingElse) { "Something else cannot be blank" }
}
companion object {
operator fun invoke(something: Int, somethingElse: String) : MyData =
MyData(something, somethingElse.replace("\n", " "))
}
}
fun main(args: Array<String>) {
val m = MyData(77, "something\nwicked\nthis\nway\ncomes")
println(m.somethingElse)
}
Prints:
something wicked this way comes
You'll note the helpful warning:
Private data class constructor is exposed via the generated 'copy' method.
This method can not be overridden (as far as I can tell) so you have to take care, still. One solution is to hide the actual data class away:
interface MyData {
val s: Int
val sE: String
private data class MyDataImpl(
override val s: Int,
override val sE: String
) : MyData {
init {
require(s > 20) { "Something must be > 20" }
require("" != sE) { "Something else cannot be blank" }
}
}
companion object {
operator fun invoke(s: Int, sE: String) : MyData =
MyDataI(s, sE.replace("\n", " "))
}
}
Now your invariant (no line breaks) is maintained, copy and other dangerous methods (if any, I haven't checked) are hidden away -- but therefore also unavailable, potentially removing some of the convenience data classes provide.
Choose your poison.
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern ="yyyy-MM-dd'T'HH:mm:ss.SSS")
is it possible to write regex in pattern? I could not
pattern ="yyyy-MM-dd'T'HH:mm:ss.SSS(Z?)"
I want to make Z as optional
any links suggestions?
I ended up creating custom deserializer based on LocalDateDeserializer.INSTANCE and moved the regex there.
After registering the deserializer the object mapper as a custom module the #JsonFormat annotation is no longer required:
#Bean
public ObjectMapper createObjectMapper() {
return new ObjectMapper()
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
.registerModule(new JavaTimeModule())
.registerModule(new CustomTimeModule());
}
and defined the deserializer in the CustomTimeModule
class CustomTimeModule extends SimpleModule {
public CustomTimeModule() {
super();
addDeserializer(LocalDate.class, CustomLocalDateDeserializer.INSTANCE);
}
}
and finally the regex part, in my case was cutting of the optional non-standard time zone that i was sometimes getting after the date, but could be easily extended to match your case:
public class CustomLocalDateDeserializer extends JSR310DateTimeDeserializerBase<LocalDate> {
private static final long serialVersionUID = 1L;
private static final DateTimeFormatter DEFAULT_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE;
public static final CustomLocalDateDeserializer INSTANCE = new CustomLocalDateDeserializer();
private CustomLocalDateDeserializer() {
this(DEFAULT_FORMATTER);
}
public CustomLocalDateDeserializer(DateTimeFormatter dtf) {
super(LocalDate.class, dtf);
}
#Override
protected JsonDeserializer<LocalDate> withDateFormat(DateTimeFormatter dtf) {
return new CustomLocalDateDeserializer(dtf);
}
#Override
public LocalDate deserialize(JsonParser parser, DeserializationContext context) throws IOException
{
if (parser.hasToken(JsonToken.VALUE_STRING)) {
String string = parser.getText().trim();
if (string.length() == 0) {
return null;
}
// >>>>>>> regex part comes here <<<<<<<
string = parser.getText().trim().substring(0, 10);
// >>>>>>> regex part comes here <<<<<<<
// as per [datatype-jsr310#37], only check for optional (and, incorrect...) time marker 'T'
// if we are using default formatter
try {
return LocalDate.parse(string, _formatter);
} catch (DateTimeException e) {
return _handleDateTimeException(context, e, string);
}
}
if (parser.isExpectedStartArrayToken()) {
JsonToken t = parser.nextToken();
if (t == JsonToken.END_ARRAY) {
return null;
}
if (context.isEnabled(DeserializationFeature.UNWRAP_SINGLE_VALUE_ARRAYS)
&& (t == JsonToken.VALUE_STRING || t==JsonToken.VALUE_EMBEDDED_OBJECT)) {
final LocalDate parsed = deserialize(parser, context);
if (parser.nextToken() != JsonToken.END_ARRAY) {
handleMissingEndArrayForSingle(parser, context);
}
return parsed;
}
if (t == JsonToken.VALUE_NUMBER_INT) {
int year = parser.getIntValue();
int month = parser.nextIntValue(-1);
int day = parser.nextIntValue(-1);
if (parser.nextToken() != JsonToken.END_ARRAY) {
throw context.wrongTokenException(parser, handledType(), JsonToken.END_ARRAY,
"Expected array to end");
}
return LocalDate.of(year, month, day);
}
context.reportInputMismatch(handledType(),
"Unexpected token (%s) within Array, expected VALUE_NUMBER_INT",
t);
}
if (parser.hasToken(JsonToken.VALUE_EMBEDDED_OBJECT)) {
return (LocalDate) parser.getEmbeddedObject();
}
// 06-Jan-2018, tatu: Is this actually safe? Do users expect such coercion?
if (parser.hasToken(JsonToken.VALUE_NUMBER_INT)) {
return LocalDate.ofEpochDay(parser.getLongValue());
}
return _handleUnexpectedToken(context, parser, "Expected array or string.");
}
I have a JSON which sends array of element in normal cases but sends empty string "" tag without array [] brackets in case of 0 elements.
How to handle this with Gson? I want to ignore the error and not cause JSONParsingException.
eg.
"types": [
"Environment",
"Management",
"Computers"
],
sometimes it returns:
"types" : ""
Getting the following exception: Expected BEGIN ARRAY but was string
Since you don't have control over the input JSON string, you can test the content and decide what to do with it.
Here is an example of a working Java class:
import com.google.gson.Gson;
import java.util.ArrayList;
public class Test {
class Types {
Object types;
}
public void test(String input) {
Gson gson = new Gson();
Types types = gson.fromJson(input,Types.class);
if(types.types instanceof ArrayList) {
System.out.println("types is an ArrayList");
} else if (types.types instanceof String) {
System.out.println("types is an empty String");
}
}
public static void main(String[] args) {
String input = "{\"types\": [\n" +
" \"Environment\",\n" +
" \"Management\",\n" +
" \"Computers\"\n" +
" ]}";
String input2 = "{\"types\" : \"\"}";
Test testing = new Test();
testing.test(input2); //change input2 to input
}
}
If a bad JSON schema is not under your control, you can implement a specific type adapter that would try to determine whether the given JSON document is fine for you and, if possible, make some transformations. I would recomment to use #JsonAdapter in order to specify improperly designed types (at least I hope the entire API is not improperly designed).
For example,
final class Wrapper {
#JsonAdapter(LenientListTypeAdapterFactory.class)
final List<String> types = null;
}
where LenientListTypeAdapterFactory can be implemented as follows:
final class LenientListTypeAdapterFactory
implements TypeAdapterFactory {
// Gson can instantiate it itself, let it just do it
private LenientListTypeAdapterFactory() {
}
#Override
public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
// Obtaining the original list type adapter
#SuppressWarnings("unchecked")
final TypeAdapter<List<?>> realListTypeAdapter = (TypeAdapter<List<?>>) gson.getAdapter(typeToken);
// And wrap it up in the lenient JSON type adapter
#SuppressWarnings("unchecked")
final TypeAdapter<T> castTypeAdapter = (TypeAdapter<T>) new LenientListTypeAdapter(realListTypeAdapter);
return castTypeAdapter;
}
private static final class LenientListTypeAdapter
extends TypeAdapter<List<?>> {
private final TypeAdapter<List<?>> realListTypeAdapter;
private LenientListTypeAdapter(final TypeAdapter<List<?>> realListTypeAdapter) {
this.realListTypeAdapter = realListTypeAdapter;
}
#Override
public void write(final JsonWriter out, final List<?> value)
throws IOException {
realListTypeAdapter.write(out, value);
}
#Override
public List<?> read(final JsonReader in)
throws IOException {
// Check the next (effectively current) JSON token
switch ( in.peek() ) {
// If it's either `[...` or `null` -- we're supposing it's a "normal" list
case BEGIN_ARRAY:
case NULL:
return realListTypeAdapter.read(in);
// Is it a string?
case STRING:
// Skip the value entirely
in.skipValue();
// And return a new array list.
// Note that you might return emptyList() but Gson uses mutable lists so we do either
return new ArrayList<>();
// Not anything known else?
case END_ARRAY:
case BEGIN_OBJECT:
case END_OBJECT:
case NAME:
case NUMBER:
case BOOLEAN:
case END_DOCUMENT:
// Something definitely unexpected
throw new MalformedJsonException("Cannot parse " + in);
default:
// This would never happen unless Gson adds a new type token
throw new AssertionError();
}
}
}
}
Here is it how it can be tested:
for ( final String name : ImmutableList.of("3-elements.json", "0-elements.json") ) {
try ( final Reader reader = getPackageResourceReader(Q43562427.class, name) ) {
final Wrapper wrapper = gson.fromJson(reader, Wrapper.class);
System.out.println(wrapper.types);
}
}
Output:
[Environment, Management, Computers]
[]
If the entire API uses "" for empty arrays, then you can drop the #JsonAdapter annotation and register the LenientListTypeAdapterFactory via GsonBuilder, but add the following lines to the create method in order not to break other type adapters:
if ( !List.class.isAssignableFrom(typeToken.getRawType()) ) {
// This tells Gson to try to pick up the next best-match type adapter
return null;
}
...
There are a lot of weirdly designed JSON response choices, but this one hits the top #1 issue where nulls or empties are represented with "". Good luck!
Thanks for all your answers.
The recommed way as mentioned in above answers would be to use TypeAdapters and ExclusionStrategy for GSON.
Here is a good example Custom GSON desrialization
While putting together a presentation, I have a situation in which I expect an exception, but I am getting none, when I run my corresponding unit test. What I am doing is incrementally modifying a bean. In this version of the Product and Accessory classes, I have removed the setters/getters for all the properties (save for a setter for one of the Product properties). I have previously converted my classes to use field access notation. So, since I have removed the setters/getters, I am expecting an exception because the field visibility modifiers are private.
Here is the Accessory class:
public class Accessory {
private String name
private BigDecimal cost
private BigDecimal price
public Accessory() {
this.cost = BigDecimal.ZERO
this.price = BigDecimal.ZERO
}
public Accessory(String name, BigDecimal cost, BigDecimal price) {
this.name = name
this.cost = cost
this.price = price
}
#Override
public int hashCode() {
final int prime = 31
int result = 1
result = prime * result + ((cost == null) ? 0 : cost.hashCode())
result = prime * result + ((name == null) ? 0 : name.hashCode())
result = prime * result + ((price == null) ? 0 : price.hashCode())
return result
}
public boolean equals(Accessory obj) {
return name == obj.name &&
cost == obj.cost &&
price == obj.price
}
#Override
public String toString() {
return "Accessory [" + "name=" + name + ", cost=" + cost + ", price=" + price + "]"
}
}
Here are snippets from the Product class:
public class Product {
private String model
private List<Accessory> accessories
private TreeMap<Integer, BigDecimal> priceBreaks
private BigDecimal cost
private BigDecimal price
...
public BigDecimal getAccessorizedCost() {
...
for (Accessory pkg : this.accessories) {
pkgCost = pkgCost.add pkg.cost
}
return pkgCost
}
...
}
I would expect that the line pkgCost = pkgCost.add pkg.cost in the above snippet would throw an exception. Likewise, I would think the following asserts in my unit test would do the same:
#Test public void canCreateDefaultInstance() {
assertNull "Default construction of class ${defaultProduct.class.name} failed to properly initialize model.", defaultProduct.model
assertTrue "Default construction of class ${defaultProduct.class.name} failed to properly initialize accessories.", defaultProduct.accessories.isEmpty()
assertTrue "Default construction of class ${defaultProduct.class.name} failed to properly initialize priceBreaks.", defaultProduct.priceBreaks.isEmpty()
assertEquals "Default construction of class ${defaultProduct.class.name} failed to properly initialize cost.", BigDecimal.ZERO, defaultProduct.cost as BigDecimal
assertEquals "Default construction of class ${defaultProduct.class.name} failed to properly initialize price.", BigDecimal.ZERO, defaultProduct.price as BigDecimal
}
Here are the metaclass properties and methods:
MetaClass Properties are:
[accessorizedCost, accessorizedPrice, class, priceBreaks]
MetaClass Methods are:
[__$swapInit, addPriceBreak, calcDiscountMultiplierFor, calcVolumePriceFor, equals, getAccessorizedCost, getAccessorizedPrice, getClass, getMetaClass, getProperty, hashCode, invokeMethod, notify, notifyAll, setMetaClass, setPriceBreaks, setProperty, toString, wait]
You can see, for example, that there are no properties nor corresponding getter/setters for the privately defined model, accessories, cost and price fields. So, like the line in the Product class not failing when referencing the cost property of Accessory, I do not understand how the unit tests can pass when there is not property nor getter/setters for these private fields.
I am compiling using Groovy 2.0.4 and running Eclipse.
What am I missing or not understanding?
I am using Google cloud end point for my rest service. I am consuming this data in a GWT web client using RestyGWT.
I noticed that cloud end point is automatically enclosing a long datatype in double quotes which is causing an exception in RestyGWT when I try to convert JSON to POJO.
Here is my sample code.
#Api(name = "test")
public class EndpointAPI {
#ApiMethod(httpMethod = HttpMethod.GET, path = "test")
public Container test() {
Container container = new Container();
container.testLong = (long)3234345;
container.testDate = new Date();
container.testString = "sathya";
container.testDouble = 123.98;
container.testInt = 123;
return container;
}
public class Container {
public long testLong;
public Date testDate;
public String testString;
public double testDouble;
public int testInt;
}
}
This is what is returned as JSON by cloud end point. You can see that testLong is serialized as "3234345" rather than 3234345.
I have the following questions.
(1) How can I remove double quotes in long values ?
(2) How can I change the string format to "yyyy-MMM-dd hh:mm:ss" ?
Regards,
Sathya
What version of restyGWT are you using ? Did you try 1.4 snapshot ?
I think this is the code (1.4) responsible for parsing a long in restygwt, it might help you :
public static final AbstractJsonEncoderDecoder<Long> LONG = new AbstractJsonEncoderDecoder<Long>() {
public Long decode(JSONValue value) throws DecodingException {
if (value == null || value.isNull() != null) {
return null;
}
return (long) toDouble(value);
}
public JSONValue encode(Long value) throws EncodingException {
return (value == null) ? getNullType() : new JSONNumber(value);
}
};
static public double toDouble(JSONValue value) {
JSONNumber number = value.isNumber();
if (number == null) {
JSONString val = value.isString();
if (val != null){
try {
return Double.parseDouble(val.stringValue());
}
catch(NumberFormatException e){
// just through exception below
}
}
throw new DecodingException("Expected a json number, but was given: " + value);
}
return number.doubleValue();
}