eclipse-ee4j / eclipse-ee4j/yasson

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

Open
#651 0 comments 0 reactions 0 assignees View on GitHub
bug
Dominant language
Java
Stars
218
Forks
109
Avg merge
1d 5h
Merged PRs (30d)
9

Description

**Describe the bug**
Using this record as example:
```java
record Example(@JsonbDateFormat(TIME_IN_MILLIS) Instant longInstant, Instant stringInstant) {}
```
and this JSON to deserialize:
```json
{ "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.(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:
```java
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:
```java
@JsonbDateFormat(TIME_IN_MILLIS)
record Example(Instant longInstant, Instant stringInstant) {}
```

Also, the annotation works fine on POJOs:
```java
class Example {
@JsonbDateFormat(TIME_IN_MILLIS)
final Instant longInstant;
final Instant stringInstant;

@JsonbCreator
Example(Instant longInstant, Instant stringInstant) { /* ... */ }

// getters,equals,hashCode...
}
```

Contributor guide

Open the contributing guide

Research direction

Start with AnnotationIntrospector.getConstructorDateFormatter and CreatorModel, which appear in the reported stack trace. Run the supplied JUnit 5 TimeInMillisRecordDeserializationTest and verify that record deserialization with a component-level TIME_IN_MILLIS annotation completes without the reported exception and preserves both Instant values.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
backend-api-design
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.