Skip to content

Deserialization of record component with @JsonbDateFormat(TIME_IN_MILLIS) fails #651

Open
@jacopo-cavallarin

Description

@jacopo-cavallarin

Describe the bug
Using this record as example:

record Example(@JsonbDateFormat(TIME_IN_MILLIS) Instant longInstant, Instant stringInstant) {}

and this JSON to deserialize:

{ "longInstant": 1728574424000, "stringInstant": "2024-10-11T13:28:22Z" }

The deserialization fails with the following exception:

jakarta.json.bind.JsonbException: Internal error: Pattern includes reserved character: '#'
	at org.eclipse.yasson.internal.DeserializationContextImpl.deserializeItem(DeserializationContextImpl.java:142)
	at org.eclipse.yasson.internal.DeserializationContextImpl.deserialize(DeserializationContextImpl.java:127)
	at org.eclipse.yasson.internal.JsonBinding.deserialize(JsonBinding.java:55)
	at org.eclipse.yasson.internal.JsonBinding.fromJson(JsonBinding.java:62)
	...
Caused by: java.lang.IllegalArgumentException: Pattern includes reserved character: '#'
	at java.base/java.time.format.DateTimeFormatterBuilder.parsePattern(DateTimeFormatterBuilder.java:2053)
	at java.base/java.time.format.DateTimeFormatterBuilder.appendPattern(DateTimeFormatterBuilder.java:1907)
	at java.base/java.time.format.DateTimeFormatter.ofPattern(DateTimeFormatter.java:593)
	at org.eclipse.yasson.internal.AnnotationIntrospector.lambda$getConstructorDateFormatter$9(AnnotationIntrospector.java:595)
	at java.base/java.util.Optional.map(Optional.java:260)
	at org.eclipse.yasson.internal.AnnotationIntrospector.getConstructorDateFormatter(AnnotationIntrospector.java:595)
	at org.eclipse.yasson.internal.model.CreatorModel.<init>(CreatorModel.java:55)
	at org.eclipse.yasson.internal.AnnotationIntrospector.createJsonbCreator(AnnotationIntrospector.java:212)
	at org.eclipse.yasson.internal.ClassMultiReleaseExtension.findCreator(ClassMultiReleaseExtension.java:55)
	at org.eclipse.yasson.internal.AnnotationIntrospector.getCreator(AnnotationIntrospector.java:189)
	at org.eclipse.yasson.internal.AnnotationIntrospector.introspectCustomization(AnnotationIntrospector.java:798)
	at org.eclipse.yasson.internal.MappingContext.lambda$createParseClassModelFunction$1(MappingContext.java:88)
	at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1713)
	at org.eclipse.yasson.internal.MappingContext.getOrCreateClassModel(MappingContext.java:77)
	at org.eclipse.yasson.internal.deserializer.DeserializationModelCreator.deserializerChain(DeserializationModelCreator.java:122)
	at org.eclipse.yasson.internal.DeserializationContextImpl.deserializeItem(DeserializationContextImpl.java:137)
	... 9 more

To Reproduce
Run the following Junit5 test:

import jakarta.json.bind.Jsonb;
import jakarta.json.bind.JsonbBuilder;
import jakarta.json.bind.annotation.JsonbDateFormat;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.time.Instant;

import static jakarta.json.bind.annotation.JsonbDateFormat.TIME_IN_MILLIS;
import static java.time.temporal.ChronoUnit.MILLIS;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;

class TimeInMillisRecordDeserializationTest {
    public record Example(@JsonbDateFormat(TIME_IN_MILLIS) Instant longInstant, Instant stringInstant) {}

    Jsonb jsonb;

    @BeforeEach
    void setUp() {
        jsonb = JsonbBuilder.create();
    }

    @AfterEach
    void tearDown() throws Exception {
        jsonb.close();
    }

    @Test
    void shouldDeserialize() {
        var expected = new Example(Instant.now().truncatedTo(MILLIS), Instant.now());

        var json = "{ \"longInstant\": %d, \"stringInstant\": \"%s\" }"
                .formatted(expected.longInstant().toEpochMilli(), expected.stringInstant());

        var actual = assertDoesNotThrow(() -> jsonb.fromJson(json, Example.class));

        assertEquals(expected, actual);
    }
}

This test will fail with the exception mentioned above.

Expected behavior
The test passes successfully.

System information:

  • OS: macOS 15.0.1
  • Java Version: 23
  • Yasson Version: 3.0.4

Additional context
Using the annotation on the record level works fine, but forces all datetime components in the record to be deserialized from epoch millis:

@JsonbDateFormat(TIME_IN_MILLIS)
record Example(Instant longInstant, Instant stringInstant) {}

Also, the annotation works fine on POJOs:

class Example {
    @JsonbDateFormat(TIME_IN_MILLIS)
    final Instant longInstant;
    final Instant stringInstant;
    
    @JsonbCreator
    Example(Instant longInstant, Instant stringInstant) { /* ... */ }

    // getters,equals,hashCode...
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working right

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions