yasson icon indicating copy to clipboard operation
yasson copied to clipboard

Deserialization of record component with `@JsonbDateFormat(TIME_IN_MILLIS)` fails

Open jacopo-cavallarin opened this issue 1 year ago • 0 comments

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...
}

jacopo-cavallarin avatar Oct 11 '24 12:10 jacopo-cavallarin