Are java records supported?

When using records for command and events, the basic Axon framework has no problems (the unit test run without a hitch). But when trying to persist the events in the Axon SE server, things have a problem;

---- Debugging information ----
cause-exception     : java.lang.UnsupportedOperationException
cause-message       : can't get field offset on a record class: private final java.lang.String nl.softworks.selfroster.domain.rosterBlock.CreateRosterBlock.rosterBlockId
class               : nl.softworks.selfroster.domain.rosterBlock.CreateRosterBlock
required-type       : nl.softworks.selfroster.domain.rosterBlock.CreateRosterBlock
converter-type      : com.thoughtworks.xstream.converters.reflection.ReflectionConverter
path                : /nl.softworks.selfroster.domain.rosterBlock.CreateRosterBlock/rosterBlockId
line number         : 1
version             : 1.4.18
-------------------------------] with root cause

org.axonframework.axonserver.connector.command.AxonServerRemoteCommandHandlingException: An exception was thrown by the remote message handling component: 
---- Debugging information ----
cause-exception     : java.lang.UnsupportedOperationException
cause-message       : can't get field offset on a record class: private final java.lang.String nl.softworks.selfroster.domain.rosterBlock.CreateRosterBlock.rosterBlockId
class               : nl.softworks.selfroster.domain.rosterBlock.CreateRosterBlock
required-type       : nl.softworks.selfroster.domain.rosterBlock.CreateRosterBlock
converter-type      : com.thoughtworks.xstream.converters.reflection.ReflectionConverter
path                : /nl.softworks.selfroster.domain.rosterBlock.CreateRosterBlock/rosterBlockId
line number         : 1
version             : 1.4.18
-------------------------------
Caused by can't get field offset on a record class: private final java.lang.String nl.softworks.selfroster.domain.rosterBlock.CreateRosterBlock.rosterBlockId
	at org.axonframework.axonserver.connector.ErrorCode.lambda$static$11(ErrorCode.java:86) ~[axon-server-connector-4.5.8.jar:4.5.8]
	at org.axonframework.axonserver.connector.ErrorCode.convert(ErrorCode.java:182) ~[axon-server-connector-4.5.8.jar:4.5.8]
	at org.axonframework.axonserver.connector.command.CommandSerializer.deserialize(CommandSerializer.java:164) ~[axon-server-connector-4.5.8.jar:4.5.8]
	at org.axonframework.axonserver.connector.command.AxonServerCommandBus.lambda$doDispatch$2(AxonServerCommandBus.java:167) ~[axon-server-connector-4.5.8.jar:4.5.8]
	at java.base/java.util.concurrent.CompletableFuture$UniApply.tryFire(CompletableFuture.java:646) ~[na:na]
	at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510) ~[na:na]
	at java.base/java.util.concurrent.CompletableFuture.complete(CompletableFuture.java:2147) ~[na:na]
	at io.axoniq.axonserver.connector.command.impl.CommandChannelImpl$CommandResponseHandler.onNext(CommandChannelImpl.java:372) ~[axonserver-connector-java-4.5.4.jar:4.5.4]
	at io.axoniq.axonserver.connector.command.impl.CommandChannelImpl$CommandResponseHandler.onNext(CommandChannelImpl.java:359) ~[axonserver-connector-java-4.5.4.jar:4.5.4]
	at io.grpc.stub.ClientCalls$StreamObserverToCallListenerAdapter.onMessage(ClientCalls.java:466) ~[grpc-stub-1.43.0.jar:1.43.0]
	at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$1MessagesAvailable.runInternal(ClientCallImpl.java:661) ~[grpc-core-1.43.0.jar:1.43.0]
	at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$1MessagesAvailable.runInContext(ClientCallImpl.java:646) ~[grpc-core-1.43.0.jar:1.43.0]
	at io.grpc.internal.ContextRunnable.run(ContextRunnable.java:37) ~[grpc-core-1.43.0.jar:1.43.0]
	at io.grpc.internal.SerializingExecutor.run(SerializingExecutor.java:133) ~[grpc-core-1.43.0.jar:1.43.0]
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136) ~[na:na]
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) ~[na:na]
	at java.base/java.lang.Thread.run(Thread.java:833) ~[na:na]

And to immediately answer my own question; xstream has support for java record classes in version 1.5.0+, currently 1.4.18 is used.

It is possible to use jackson for event serialization only, which does support records. See the configuration in:

And let me immediately introduce the next problem: as of Java 15 the serialization still fails on a method call in the JDK’s Unsafe class.

cause-exception : java.lang.UnsupportedOperationException
cause-message : can’t get field offset on a record class: private final java.lang.String nl.softworks.selfroster.domain.rosterBlock.CreateRosterBlock.rosterBlockId

    public long objectFieldOffset(Field f) {
        if (f == null) {
            throw new NullPointerException();
        }
        Class<?> declaringClass = f.getDeclaringClass();
        if (declaringClass.isHidden()) {
            throw new UnsupportedOperationException("can't get field offset on a hidden class: " + f);
        }
        if (declaringClass.isRecord()) {
            throw new UnsupportedOperationException("can't get field offset on a record class: " + f);
        }
        return theInternalUnsafe.objectFieldOffset(f);
    }

The exception suggests you’re (still) using XStream to serialize records.
Your previous message suggests you configured Jackson as the Event Serializer, but it looks like XStream is still being used for it.

Ah, indeed I see the serializer class.

My configuration was wrong; it’s “messageS”. Also, Jackson apparently cannot serialize classes without an instance variable (my query classes don’t have any, I had to put a dummy in there). But then it indeed
works with records. Thanks for pointing that out.

Need to figure out how to get this in the Jackson config.

And that is done by adding the following to the application.properties:

spring.jackson.serialization.fail-on-empty-beans=false

And the query classes can become record classes as well.

And using Jackson it is also possible to use immutables.org, which also generates builders thus making the creation of commands and events less fragile.
JSON serialization

But also more verbose, unless @Value.Parameter is used

CreateRosterBlock.of(orderId)

Which also makes creating an event out of a command possible in one place:

	static public RosterBlockCreated of(CreateRosterBlock createRosterBlock) {
		return RosterBlockCreated.of(createRosterBlock.rosterBlockId());
	}