Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for 💾 Java 14 Records 🐞 with JakartaEE JSON-B
Carlos Chacin ☕👽
Carlos Chacin ☕👽

Posted on

     

💾 Java 14 Records 🐞 with JakartaEE JSON-B

The article was initially published atcarloschac.in

💾 Java 14 Records 🐞 with JakartaEE JSON-B

In the previous article about Java 14 Records, we saw how to start creating Records to avoid writing much boilerplate code that the compiler would generate for us.

Now the next steps are to see how we can serialize records to JSON and deserialize JSON to records to be able to use them as a request/response representation for microservices.

In this case, we would use the JSON-B specification used by JakartaEE and MicroProfile implementations like GlashFish, TomEE, Wildfly, OpenLiberty, and Quarkus.

Continuing with thesame example that we used in the previous article, we would need to add a JSON-B implementation, let's add the Eclipse Yasson dependency to our existing pom.xml file:

<dependency><groupId>org.eclipse</groupId><artifactId>yasson</artifactId><version>1.0.6</version><scope>test</scope></dependency>
Enter fullscreen modeExit fullscreen mode

💾 Now let's see our example Record:

recordPerson(StringfirstName,StringlastName,Stringaddress,LocalDatebirthday,List<String>achievements){}
Enter fullscreen modeExit fullscreen mode

💡 Java 14+ compiler would generate all of the following:

$javap-p Person.class
Enter fullscreen modeExit fullscreen mode
publicfinalclassPersonextendsjava.lang.Record{privatefinaljava.lang.StringfirstName;privatefinaljava.lang.StringlastName;privatefinaljava.lang.Stringaddress;privatefinaljava.time.LocalDatebirthday;privatefinaljava.util.List<java.lang.String>achievements;publicPerson(java.lang.String,java.lang.String,java.lang.String,java.time.LocalDate,java.util.List<java.lang.String>);publicjava.lang.StringtoString();publicfinalinthashCode();publicfinalbooleanequals(java.lang.Object);publicjava.lang.StringfirstName();publicjava.lang.StringlastName();publicjava.lang.Stringaddress();publicjava.time.LocalDatebirthday();publicjava.util.List<java.lang.String>achievements();}
Enter fullscreen modeExit fullscreen mode

🔨 Our test case would consist of using Yasson

To serialize a record like this:

varperson=newPerson("John","Doe","USA",LocalDate.of(1990,11,11),List.of("Speaker"));
Enter fullscreen modeExit fullscreen mode

To a json output like this:

{"achievements":["Speaker"],"address":"USA","birthday":"1990-11-11","firstName":"John","lastName":"Doe"}
Enter fullscreen modeExit fullscreen mode

🔎 Let's check the complete test code:

@TestvoidserializeRecords()throwsException{// Givenvarperson=newPerson("John","Doe","USA",LocalDate.of(1990,11,11),List.of("Speaker"));varexpected="""            {                "achievements": [                    "Speaker"                ],                "address": "USA",                "birthday": "1990-11-11",                "firstName": "John",                "lastName": "Doe"            }""";// Whenvarjsonb=JsonbBuilder.create(newJsonbConfig().withFormatting(true));varserialized=jsonb.toJson(person);// ThenassertThat(serialized).isEqualTo(expected);}
Enter fullscreen modeExit fullscreen mode

After running the test with maven or in the IDE, we get the following assertion's failure:

org.opentest4j.AssertionFailedError:Expecting: <"{}">to be equal to: <"{    "achievements": [        "Speaker"    ],    "address": "USA",    "birthday": "1990-11-11",    "firstName": "John",    "lastName": "Doe"}">but was not.
Enter fullscreen modeExit fullscreen mode

JSON-B is serializing the record as an empty object :(

Let's read the specification:

For a serialization operation, if a matching public getter method exists, the method is called to obtain the value of the property. If a matching getter method with private, protected or defaulted to package-only access exists, then this field is ignored. If no matching getter method exists and the field is public, then the value is obtained directly from the field.

Let's take a look at the fieldfirstName on the record as an example:

$javap-p Person.class
Enter fullscreen modeExit fullscreen mode
publicfinalclassPersonextendsjava.lang.Record{privatefinaljava.lang.StringfirstName;// This field is private...publicjava.lang.StringfirstName();// This property is not a getter, i.e. getFirstName()
Enter fullscreen modeExit fullscreen mode

We are not complaining with the specification, but we can solve this renaming the field defined in the record to use the JavaBean setters convention:

recordPerson(StringgetFirstName,StringgetLastName,StringgetAddress,LocalDategetBirthday,List<String>getAchievements){}
Enter fullscreen modeExit fullscreen mode

⚠️ The above example can be problematic if we forget about adding theget prefix. For primitiveboolean properties, the prefix for getters isis instead ofget, also easy to forget.

Another alternative is to specify to the JSON-B implementation to serialize using private fields instead of the getters, for that we need to implement thePropertyVisibilityStrategy interface.

Provides mechanism on how to define customized property visibility strategy.
This strategy can be set viaJsonbConfig.

🔧 The interface has two methods:

  • boolean isVisible(Field field) Responds whether the given field should be considered as the JsonbProperty.
  • boolean isVisible(Method method) Responds whether the given method should be considered as the JsonbProperty.

To achieve our goal, we want to returntrue to serialize fields andfalse to serialize methods:

varvisibilityStrategy=newPropertyVisibilityStrategy(){@OverridepublicbooleanisVisible(Fieldfield){returntrue;}@OverridepublicbooleanisVisible(Methodmethod){returnfalse;}};
Enter fullscreen modeExit fullscreen mode

And then we pass it to theJsonbConfig object:

varjsonb=JsonbBuilder.create(newJsonbConfig().withFormatting(true).withPropertyVisibilityStrategy(visibilityStrategy));
Enter fullscreen modeExit fullscreen mode

♻️ Now we have a passing test:

records-jsonb-passing-test

💊 Testing Deserialization

Let's use the same record to try the deserialization using also the same configuration, the only thing that we need to add to our test is the deserialization itself using theJsonb object and the assertion to compare:

@TestvoidserializeRecords()throwsException{// Givenvarperson=newPerson("John","Doe","USA",LocalDate.of(1990,11,11),List.of("Speaker"));varjson="""            {                "achievements": [                    "Speaker"                ],                "address": "USA",                "birthday": "1990-11-11",                "firstName": "John",                "lastName": "Doe"            }""";// WhenvarvisibilityStrategy=newPropertyVisibilityStrategy(){@OverridepublicbooleanisVisible(Fieldfield){returntrue;}@OverridepublicbooleanisVisible(Methodmethod){returnfalse;}};varjsonb=JsonbBuilder.create(newJsonbConfig().withFormatting(true).withPropertyVisibilityStrategy(visibilityStrategy));varserialized=jsonb.toJson(person);vardeserialized=jsonb.fromJson(json,Person.class);// ThenassertThat(deserialized).isEqualTo(person);assertThat(serialized).isEqualTo(json);}
Enter fullscreen modeExit fullscreen mode

Our test case fails for deserialization with the following error:

javax.json.bind.JsonbException:Cannotcreateaninstanceofaclass:classrecords.Person,Nodefaultconstructorfound.
Enter fullscreen modeExit fullscreen mode

📌 Per the specification, we need either to:

  • ✅ Define an empty/null constructor
  • ✅ Define a constructor or method annotated with@JsonbCreator

But how can we do that if the compiler generates the constructor for a record?

💾 Java Records 👷 Compact Constructor:

recordPerson(StringfirstName,StringlastName,Stringaddress,LocalDatebirthday,List<String>achievements){publicPerson{if(birthday>=LocalDate.now()){thrownewIllegalArgumentException("Birthday must be < today!");}}}
Enter fullscreen modeExit fullscreen mode

This compact constructor is meant to be used only for validation purposes as in the example above and notice that we don't have to repeat the field parameters nor the field initializations, The remaining initialization code is supplied by the compiler.

But how this helps to our deserialization problem, well, as another regular constructor we can add annotations there, let's fix it:

recordPerson(StringfirstName,StringlastName,Stringaddress,LocalDatebirthday,List<String>achievements){@JsonbCreatorpublicPerson{}}
Enter fullscreen modeExit fullscreen mode

This is the decompiled bytecode generated by the compiler:

publicfinalclassPersonextendsjava.lang.Record{privatefinaljava.lang.StringfirstName;privatefinaljava.lang.StringlastName;privatefinaljava.lang.Stringaddress;privatefinaljava.time.LocalDatebirthday;privatefinaljava.util.List<java.lang.String>achievements;@javax.json.bind.annotation.JsonbCreatorpublicPerson(java.lang.StringfirstName,java.lang.StringlastName,java.lang.Stringaddress,java.time.LocalDatebirthday,java.util.List<java.lang.String>achievements){/* compiled code */}publicjava.lang.StringtoString(){/* compiled code */}publicfinalinthashCode(){/* compiled code */}publicfinalbooleanequals(java.lang.Objecto){/* compiled code */}publicjava.lang.StringfirstName(){/* compiled code */}publicjava.lang.StringlastName(){/* compiled code */}publicjava.lang.Stringaddress(){/* compiled code */}publicjava.time.LocalDatebirthday(){/* compiled code */}publicjava.util.List<java.lang.String>achievements(){/* compiled code */}}
Enter fullscreen modeExit fullscreen mode

We can notice the@JsonbCreator annotation passed to the generated constructor. After that change, our test suite for serialization and deserialization of records with JSON-B passes.

🔆 Conclusions

  • ✅ We can use records to serialize and deserialize JSON request/response objects.
  • ✅ We described two ways of achieving serialization:
    • ✅ Renaming the fields to use the getter convention.
    • ✅ Adding a customPropertyVisibilityStrategy to serialize using the private fields.
  • ✅ We described how to achieve the deserialization using the@JsonbCreator annotation.
  • 🔴 Most probably, in the following versions of the JSON-B API specification records would be taken in consideration and the strategies described in this article are not going to be required.

Top comments(2)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
dougbaughman profile image
DougBaughman
  • Joined

Thanks Carlos, very helpful.
Records seem to be the perfect fit for DTO's. When I was first testing my code from my IDE all was good. Then running against the packaged jar file on my laptop was OK as well. But when I pushed the jar to the cloud, I started getting the deserialization errors mentioned here (different JRE?). The @JsonbCreator annotation and PropertyVisibilityStrategy configuration added to the serializer solved the problem.

CollapseExpand
 
citronbrick profile image
CitronBrick
Developper
  • Work
    Junior Front End Engineer
  • Joined

A very relevant article. Would love to try it out someday.

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

Java Dev | Husband | Father-------
  • Location
    Seattle, WA
  • Education
    Computer Science
  • Work
    Software Developer Engineer
  • Joined

More fromCarlos Chacin ☕👽

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp