Jackson-databind: Support for JDK 14 record types

Created on 3 May 2020  路  15Comments  路  Source: FasterXML/jackson-databind

(note: created based on https://github.com/FasterXML/jackson-future-ideas/issues/46)

With JDK 14 Java will get Record type of classes. While it appears that they can already be used with right set of configuration options, it would probably make sense to add explicit support to maybe use different auto-detect defaults.
Support should be done in a way that does not require JDK 14 dependency by jackson-databind (that is a leap too far even for 3.0 at least as of May 2020): either so that

  1. If possible, detection simply uses existing JDK functionality (assuming enough features are translated into bytecode accessible using JDK 8 methods), OR
  2. If support does require JDK 14 API, create another module (jackson-module-jdk14 or jackson-datatype-jdk14, depending).

Marking tentatively as 3.0, but may be moved to a 2.x version if and when someone actual works on this.

2.12 most-wanted

Most helpful comment

Released as part of Jackson 2.12.0.

All 15 comments

Can Jackson make use of the actual (parameterized) record constructor? That'd definitely be preferable over injecting values into private record fields.

@gunnarmorling yes that would be optimal: Kotlin module does that for data classes at least, I think Scala module similarly for case classes. That's where detecting Record types would help, to forcible enable use of constructor (and possibly figure out main one, if class definition has multiple alternatives), without changing behavior of POJO types (where there are backwards-compatibility issues, possible security concerns).

Re identifying the canonical constructor, this requires a small amout of bespoke logic to collect the component types: https://twitter.com/sormuras/status/1258657911263428608. The biggest challenge I see (without knowing internals of Jackson at all) is that object graphs (records referencing other records referencing other records...) need to be built starting at leaf nodes, so that the inner nodes can be passed to constructors of outer nodes. Is there some logic for that already?

@gunnarmorling I am not sure I (yet) understand the specific issue, but I suspect answer is no: Jackson simply iterates over constructors it sees and has to determine it in "shallow" manner (deserializers for delegated type are resolved in separate passes to allow for cyclic type dependencies), without much coupling between levels.

Then again, traversing constructors could perhaps be encapsulated without most of databinding being aware of the logic. Still, if there are multiple non-zero-arg constructors that gets tricky and is not a supported use case yet.

It might well be there's no problem at all. What I was thinking of was this case:

{
    "name" : "foo",
    "bar" : { "name" : "bar" }
}

As records are immutable, the nested Bar must be instantiated first, so it can be passed when invoking the Foo(String, Bar) constructor.

@youribonnaffe, yes it is. Nice, seems records are nicely supported than in Jackson already 馃憤

Would it be possible to support method local records as well? Currently in BeanDeserializerFactory::isPotentialBeanType there is a check preventing that:

        /* also: can't deserialize some local classes: static are ok; in-method not;
         * other non-static inner classes are ok
         */
        typeStr = ClassUtil.isLocalType(type, true);
        if (typeStr != null) {
            throw new IllegalArgumentException("Cannot deserialize Class "+type.getName()+" (of type "+typeStr+") as a Bean");
        }

Until official record support here is an annotation introspection that can be used as workaround:

public class JavaRecordAnnotationIntrospector extends NopAnnotationIntrospector {
    public JavaRecordAnnotationIntrospector() {
    }

    @Override
    public String findImplicitPropertyName(AnnotatedMember member) {
        Class<?> declaringClass = member.getDeclaringClass();
        if (declaringClass.isRecord()) {
            if (member instanceof AnnotatedMethod am) {
                RecordComponent[] recordComponents = declaringClass.getRecordComponents();
                for (RecordComponent recordComponent : recordComponents) {
                    if (recordComponent.getAccessor().equals(am.getAnnotated())) {
                        return recordComponent.getName();
                    }
                }
            }
        }
        return null;
    }
}

Hi @jedvardsson, it looks similar to https://gist.github.com/youribonnaffe/03176be516c0ed06828ccc7d6c1724ce which was one of my first attempt to support it

Some progress here: was able to make serialization work as expected by introduction of new handler (see #2800) and Record-specific branching with default one.
Now working on deserialization side, trying to figure out a clean way to plug that in -- existing Creator introspection code is horrible and in dire need of rewriting, but I'll have to find a compromise wherein I'll branch Record-specific logic into clean(er) new implementation but will not change non-Record POJO handling. This after digging into code for a while and realizing that neither well-placed hack nor full-scale rewrite will be practical. :)
Hoping to get it wrapped this weekend, either way.

And there! It is done -- initial implementation at any rate.

If anyone wants to check it out that would be highly appreciated. The highest value contribution would probably be addition of tests in RecordTest under src/test-jdk14/java (same test class or new ones whatever) -- I suspect that there may be edge cases wrt handling of annotations, mostly on deserialization side.

Released as part of Jackson 2.12.0.

Very nice. Is the usage documented somewhere?

Hello @vogella,
The usage is pretty much what you can expect from a POJO, take a record it will serialize/deserialize out of the box.
There are a few examples in the unit tests: https://github.com/FasterXML/jackson-databind/blob/master/src/test-jdk14/java/com/fasterxml/jackson/databind/RecordTest.java

Do you think it would be worth its own documentation?
I would be ok to help on that. Where would such documentation live? In the README or in https://github.com/FasterXML/jackson-docs?

Also @cowtowncoder published a blog post on the latest release where the feature is mentioned: https://cowtowncoder.medium.com/jackson-2-12-features-eee9456fec75

Was this page helpful?
0 / 5 - 0 ratings