A long-standig bug in JakartaEE’s JSON-B implementations (JSON Binding) cause me some headaches. Luckily, I was able to track it down. Here is how to reproduce it and why you might hit it sooner rather than later.
In short: The Property Naming Strategy is not applied to Java Records containing a @JsonbConstructor
annotation. This bug exists in both implementations, Apache Johnzon and Eclipse Yasson.
PropertyVisibilityStrategy: Java 17 Records
To (de)serialize records, you need to modify the PropertyVisibilityStrategy
. This strategy defines how properties, which should get (de-)serialized should be discovered by the JSON-B implementation. Because JSON-B implementations are made for Java beans (getters and setters), and Java Records broke this pattern by omitting the get…()
in the method names, you first need to define a Visibility Strategy which picks up those internal properties:
static class PrivateVisibilityStrategy implements PropertyVisibilityStrategy {
@Override
public boolean isVisible(Field field) { return true; }
@Override
public boolean isVisible(Method method) { return false; }
}
Serialization will work now. For an imaginary record Person
:
{
"firstName": "Richard",
"lastName": "Feynman"
}
Add PropertyNamingStrategy into the mix
Now, think of all your lowerCamelCase
methods (and properties): Those are not too typical for JSON. In fact, most JSON you encounter written in Python or PHP or JavaScript or TypeScript will instead use lower_snake_case
for property naming. To »comply« with this de-factor standard, you can make JSON-B implementations rename and map your JSON-B field names accordingly:
new JsonbConfig()
.withNullValues(Boolean.TRUE)
.withFormatting(Boolean.TRUE)
.withPropertyVisibilityStrategy(new PrivateVisibilityStrategy())
.withPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CASE_WITH_UNDERSCORES)
;
The last line calls the built-in LOWER_CASE_WITH_UNDERSCORES
which every JSON-B implementation must support.
Now you properties will be serialized like this:
{
"first_name": "Richard",
"last_name": "Feynman"
}
Deserializing with @JsonbConstructor
Now the funny part: Deserializing. By default, JSON-B implementations won’t be able to do that: They need a non-private no-arg-constructor, which just is not available with Records. An alternative to this is either a static factory method OR an annotated constructor. While both could work for records, there is a neat trick: Records in Java can be written with a no-arg-constructor, which is called with all the fields. That’s a little bit of magic there, but it works just fine. So the following class should work:
public record Person(String firstName, String lastName) {
@JsonbCreator
public Author {
requireNonNull(firstName, "firstName");
requireNonNull(lastName, "lastName");
}
}
Actually, this works (in a sense): JSON-B will not complain anymore about missing no-arg constructors and happily use the provided one instead. It will also recognize the parameters as JSON properties.
BUT.
We still get an exception:
jakarta.json.bind.JsonbException:
Author cannot be constructed to deserialize json object value: {
"birth_date":"1918-05-11",
"first_name":"Ric...
java.lang.NullPointerException: firstName
jakarta.json.bind.JsonbException: JsonbCreator parameter firstName is missing in json document.
jakarta.json.bind.JsonbException: Exception occurred during call to JSONB creator on class: class io.github.bmarwell.jsonb.creator.value.Author.
Caused by: java.lang.reflect.InvocationTargetException
Caused by: java.lang.NullPointerException: firstName
Huh? As you can see from the Yasson 2 error message, all the implementations will look for a field firstName
instead of first_name
, although we defined a differen PropertyNamingStrategy. Debugging Yasson3 and Apache Johnzon reveals that they are looking for the field firstName
as well.
Talking to Romain about the bug
After I made sure this should have worked (yes, I found no different opinion in the documentation), I talked to Romain Manni-Bucau, who is one of the core developers of Apache Johnzon. He agreed that this is surprising and probably unintentional behaviour. In fact, while talking to him on the Apache Slack, he created a JIRA issue and fixed the bug before I was even able to set up a test case. What a magician 🧙🏻🪄 he is, indeed!
To make sure it works for my use case, I finished my test reproducer project. If you are initerested, you can test it with all four of the implementations:
# Eclipse Yasson 2.0.4
./mvnw verify -Pyasson
# Eclipse Yasson 3.0.2
./mvnw verify -Pyasson3
# Apache Johnzon 1.2.19
./mvnw verify -Pjohnzon
# Apache Johnzon 1.2.20-SNAPSHOT (bug fixed)
./mvnw verify -Pjohnzon-snapshot
The results are as expected:
Reporting an upstream bug
Now, we have a behaviour convergence in both implementations. Running my app on an application Server which ships a recent version of Apache Johnzon will work, but it would not run on any other application servers.
To remedy this, I also opened a bug at eclipse-ee4j/yasson/issues/583.
Fixing OpenLiberty using the bells-Feature
To have the patch ready RIGHT NOW in Open Liberty, you can use the bells feature:
<?xml version="1.0" encoding="UTF-8"?>
<server description="new server">
<featureManager>
<feature>jsonbContainer-2.0</feature>
<feature>jsonpContainer-2.0</feature>
</featureManager>
<bell libraryRef="johnzon"/>
<library id="johnzon">
<fileset dir="${server.config.dir}/lib/johnzon" includes="*.jar"/>
</library>
</server>
If you are using the liberty-maven-plugin, you cannot use that to download the libraries, as it does not (yet) support classifiers properly. There is a workaround (not defining a version, so it will look into the project dependencies), but I do not like it as it does things behind your back and may end up with surprising behaviour. It will also introduce problems when you have two similar artefacts with just different classifiers.
That said, this is how you download the libraries for Apache Johnzon properly:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<build>
<plugins>
<plugin>
<groupId>io.openliberty.tools</groupId>
<artifactId>liberty-maven-plugin</artifactId>
<version>3.6.1</version>
<executions>
<execution>
<id>prepare-liberty-server</id>
<phase>process-test-resources</phase>
<goals>
<goal>create</goal>
<goal>deploy</goal>
<goal>install-feature</goal>
</goals>
</execution>
<execution>
<id>pre-it</id>
<phase>pre-integration-test</phase>
<goals>
<goal>start</goal>
</goals>
</execution>
<execution>
<id>post-it</id>
<phase>post-integration-test</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
<configuration>
<features>
<acceptLicense>true</acceptLicense>
</features>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-johnzon-because-liberty-doesnt-know-about-classifiers</id>
<phase>process-test-resources</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<outputDirectory>${server.dir}/lib/johnzon</outputDirectory>
<artifactItems>
<item>
<groupId>org.apache.johnzon</groupId>
<artifactId>johnzon-core</artifactId>
<classifier>jakarta</classifier>
<version>${dependency.johnzon.version}</version>
</item>
<item>
<groupId>org.apache.johnzon</groupId>
<artifactId>johnzon-jsonb</artifactId>
<classifier>jakarta</classifier>
<version>${dependency.johnzon.version}</version>
</item>
<item>
<groupId>org.apache.johnzon</groupId>
<artifactId>johnzon-mapper</artifactId>
<classifier>jakarta</classifier>
<version>${dependency.johnzon.version}</version>
</item>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
The configuration is a little complicated, because we need to run the maven-dependency-plugin
AFTER the server is created but BEFORE it is started (for obvious reasons). Because we never know in which order the plugins were defined (thus defining the order in which they run their executions of the same phase), we need to fiddle around with different phases. For a project which only exists for integration testing, this is easily possible.
But if it is also the project you are developing your web archive with, we better get the classifier introduced into Open Liberty’s maven plugin. Guess what: Issue opened and PR opened.
Conclusion
As you can see from this, digging into a single issue can bring up a long tail of more problems which need to be addressed. However, with the help of Romain and Cheryl those issues will be gone very soon, which I am very happy about. In any case I sincerely hope this blog article will help you to see how to dig into and report a bug for Apache Software, Eclipse Libraries and even IBM’s Open Liberty server (and in this case the liberty-maven-plugin).
If you have any questions about this, please feel free to ask me on Twitter or on Mastodon.
Web links
Reproducer project:
https://github.com/bmarwell/jsonb-creator-property-namingEclipse Yasson ug report:
https://github.com/eclipse-ee4j/yasson/issues/583Apache Johnzon bug report:
https://issues.apache.org/jira/browse/JOHNZON-390JAX-RS test showcase project which shows Open Liberty integration:
https://github.com/bmarwell/jaxrs-test-showcase