Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug Fix: Failed to deserialize the date field from a number using the @JsonFormat annotation #927

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ abstract class JsonCompileSpec extends AbstractTypeElementSpec implements JsonSp
}

ApplicationContext buildContext(String className, @Language("java") String source, Map<String, Object> properties) {
return buildContext(className, source, properties, [:])
}

ApplicationContext buildContext(String className, @Language("java") String source, Map<String, Object> properties, Map contextProperty) {
ApplicationContext context =
buildContext(className, source, true)
buildContext(className, source, true, contextProperty)

jsonMapper = context.getBean(JsonMapper)

Expand Down Expand Up @@ -72,14 +76,18 @@ abstract class JsonCompileSpec extends AbstractTypeElementSpec implements JsonSp
return context
}

@Override
ApplicationContext buildContext(String className, @Language("java") String cls, boolean includeAllBeans) {
def context = super.buildContext(className, cls, true)
ApplicationContext buildContext(String className, @Language("java") String cls, boolean includeAllBeans, Map contextProperty) {
def context = super.buildContext(className, cls, true, contextProperty)
Thread.currentThread().setContextClassLoader(context.classLoader)
jsonMapper = context.getBean(JsonMapper)
return context
}

@Override
ApplicationContext buildContext(String className, @Language("java") String cls, boolean includeAllBeans) {
return buildContext(className, cls, includeAllBeans, [:])
}

@Override
ApplicationContext buildContext(String className, @Language("java") String cls) {
def context = super.buildContext(className, cls, true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,145 +15,22 @@
*/
package io.micronaut.serde.jackson


import io.micronaut.serde.config.SerdeConfiguration.NumericTimeUnit
import spock.lang.Unroll

import java.sql.Timestamp
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.OffsetDateTime
import java.time.Year
import java.time.ZoneOffset
import java.time.ZoneId
import java.time.ZonedDateTime

abstract class JsonFormatSpec extends JsonCompileSpec {

void "test disable validation"() {
when:
def i = buildBeanIntrospection('jsongetterrecord.Test', """
package jsongetterrecord;

import io.micronaut.serde.annotation.Serdeable;
import com.fasterxml.jackson.annotation.JsonFormat;


@Serdeable(validate=false)
record Test(
@JsonFormat(pattern="bunch 'o junk")
int value) {
}
""")

then:
i != null
}

@Unroll
void "test fail compilation when invalid format applied to number for type #type"() {
when:
buildBeanIntrospection('jsongetterrecord.Test', """
package jsongetterrecord;

import io.micronaut.serde.annotation.Serdeable;
import com.fasterxml.jackson.annotation.JsonFormat;


@Serdeable
record Test(
@JsonFormat(pattern="bunch 'o junk")
$type.name value) {
}
""")

then:
def e = thrown(RuntimeException)
e.message.contains("Specified pattern [bunch 'o junk] is not a valid decimal format. See the javadoc for DecimalFormat: Malformed pattern \"bunch 'o junk\"")

where:
type << [Integer, int.class]
}

@Unroll
void "test fail compilation when invalid format applied to date for type #type"() {
when:
buildBeanIntrospection('jsongetterrecord.Test', """
package jsongetterrecord;

import io.micronaut.serde.annotation.Serdeable;
import com.fasterxml.jackson.annotation.JsonFormat;


@Serdeable
record Test(
@JsonFormat(pattern="bunch 'o junk")
$type.name value) {
}
""")

then:
def e = thrown(RuntimeException)
e.message.contains("Specified pattern [bunch 'o junk] is not a valid date format. See the javadoc for DateTimeFormatter: Unknown pattern letter: b")

where:
type << [LocalDateTime]
}

@Unroll
void "test json format for #type and settings #settings with record"() {
given:
def context = buildContext("""
package test;

import io.micronaut.serde.annotation.Serdeable;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.OptBoolean;

@Serdeable
record Test(
@JsonFormat(${settings.collect { "$it.key=\"$it.value\"" }.join(",")})
$type.name value
) {}
""")
expect:
def beanUnderTest = newInstance(context, 'test.Test', value)
def typeUnderTest = argumentOf(context, 'test.Test')
writeJson(jsonMapper, beanUnderTest) == result
def read = jsonMapper.readValue(result, typeUnderTest)
typeUnderTest.type.isInstance(read)
read.value == value

cleanup:
context.close()

where:
type | value | settings | result
// locale
Double | 100000.12d | [pattern: '$###,###.###', locale: 'de_DE'] | '{"value":"$100.000,12"}'

// without lo
byte | 10 as byte | [pattern: '$###,###.###'] | '{"value":"$10"}'
Byte | 10 as byte | [pattern: '$###,###.###'] | '{"value":"$10"}'
int | 10 | [pattern: '$###,###.###'] | '{"value":"$10"}'
Integer | 10 | [pattern: '$###,###.###'] | '{"value":"$10"}'
long | 100000l | [pattern: '$###,###.###'] | '{"value":"$100,000"}'
Long | 100000l | [pattern: '$###,###.###'] | '{"value":"$100,000"}'
short | 10000 as short | [pattern: '$###,###.###'] | '{"value":"$10,000"}'
Short | 10000 as short | [pattern: '$###,###.###'] | '{"value":"$10,000"}'
double | 100000.12d | [pattern: '$###,###.###'] | '{"value":"$100,000.12"}'
Double | 100000.12d | [pattern: '$###,###.###'] | '{"value":"$100,000.12"}'
float | 100000.12f | [pattern: '$###,###.###'] | '{"value":"$100,000.117"}'
Float | 100000.12f | [pattern: '$###,###.###'] | '{"value":"$100,000.117"}'
BigDecimal | new BigDecimal("100000.12") | [pattern: '$###,###.###'] | '{"value":"$100,000.12"}'
BigDecimal | new BigDecimal("100000.12") | [pattern: '$###,###.###'] | '{"value":"$100,000.12"}'
BigInteger | new BigInteger("100000") | [pattern: '$###,###.###'] | '{"value":"$100,000"}'
BigInteger | new BigInteger("100000") | [pattern: '$###,###.###'] | '{"value":"$100,000"}'

}

@Unroll
void "test json format for #type and settings #settings"() {
void "test deserialize json number format for date #type"() {
given:
def context = buildContext('test.Test', """
package test;
Expand All @@ -173,86 +50,31 @@ class Test {
return value;
}
}
""", [value: value])
expect:
writeJson(jsonMapper, beanUnderTest) == result
def read = jsonMapper.readValue(result, typeUnderTest)
typeUnderTest.type.isInstance(read)
read.value == value

cleanup:
context.close()

where:
type | value | settings | result
// locale
Double | 100000.12d | [pattern: '$###,###.###', locale: 'de_DE'] | '{"value":"$100.000,12"}'

// without locale
byte | 10 | [pattern: '$###,###.###'] | '{"value":"$10"}'
Byte | 10 | [pattern: '$###,###.###'] | '{"value":"$10"}'
int | 10 | [pattern: '$###,###.###'] | '{"value":"$10"}'
Integer | 10 | [pattern: '$###,###.###'] | '{"value":"$10"}'
long | 100000l | [pattern: '$###,###.###'] | '{"value":"$100,000"}'
Long | 100000l | [pattern: '$###,###.###'] | '{"value":"$100,000"}'
short | 10000 | [pattern: '$###,###.###'] | '{"value":"$10,000"}'
Short | 10000 | [pattern: '$###,###.###'] | '{"value":"$10,000"}'
double | 100000.12d | [pattern: '$###,###.###'] | '{"value":"$100,000.12"}'
Double | 100000.12d | [pattern: '$###,###.###'] | '{"value":"$100,000.12"}'
float | 100000.12f | [pattern: '$###,###.###'] | '{"value":"$100,000.117"}'
Float | 100000.12f | [pattern: '$###,###.###'] | '{"value":"$100,000.117"}'
BigDecimal | new BigDecimal("100000.12") | [pattern: '$###,###.###'] | '{"value":"$100,000.12"}'
BigDecimal | new BigDecimal("100000.12") | [pattern: '$###,###.###'] | '{"value":"$100,000.12"}'
BigInteger | new BigInteger("100000") | [pattern: '$###,###.###'] | '{"value":"$100,000"}'
BigInteger | new BigInteger("100000") | [pattern: '$###,###.###'] | '{"value":"$100,000"}'
""", [:], ['micronaut.serde.numeric-time-unit': timeUnit])

}

@Unroll
void "test json format for date #type and settings #settings"() {
given:
def context = buildContext('test.Test', """
package test;

import io.micronaut.serde.annotation.Serdeable;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.OptBoolean;

@Serdeable
class Test {
@JsonFormat(${settings.collect { "$it.key=\"$it.value\"" }.join(",")})
private $type.name value;
public void setValue($type.name value) {
this.value = value;
}
public $type.name getValue() {
return value;
}
def jsonString = """
{
"value": ${value}
}
""", [value: value])
def result = writeJson(jsonMapper, beanUnderTest)
def read = jsonMapper.readValue(result, typeUnderTest)
"""
def read = jsonMapper.readValue(jsonString, typeUnderTest)

expect:
result.startsWith('{"value":"') // was serialized as string, not long
typeUnderTest.type.isInstance(read)
resolver(read.value) == resolver(value)
resolver(read.value) == expected

cleanup:
context.close()

where:
type | value | settings | resolver
Instant | Instant.now() | [pattern: "yyyy-MM-dd'T'HH:mm:ss.SSSZ"] | { Instant i -> i.toEpochMilli() }
Date | new Date() | [pattern: "yyyy-MM-dd'T'HH:mm:ss.SSSZ"] | { Date d -> d.time }
java.sql.Date | new java.sql.Date(2021, 9, 15) | [pattern: "yyyy-MM-dd"] | { java.sql.Date d -> d }
Timestamp | new Timestamp(System.currentTimeMillis()) | [pattern: "yyyy-MM-dd'T'HH:mm:ss.SSSZ"] | { Timestamp d -> d }
LocalTime | LocalTime.now() | [pattern: "HH:mm:ss"] | { LocalTime i -> i.toSecondOfDay() }
LocalDate | LocalDate.now() | [pattern: "yyyy-MM-dd"] | { LocalDate d -> d }
LocalDateTime | LocalDateTime.now() | [pattern: "yyyy-MM-dd'T'HH:mm:ss.SSS"] | { LocalDateTime i -> i.toInstant(ZoneOffset.from(ZoneOffset.UTC)).toEpochMilli() }
ZonedDateTime | ZonedDateTime.now() | [pattern: "yyyy-MM-dd'T'HH:mm:ss.SSSZ"] | { ZonedDateTime i -> i.toInstant().toEpochMilli() }
OffsetDateTime | OffsetDateTime.now() | [pattern: "yyyy-MM-dd'T'HH:mm:ss.SSSZ"] | { OffsetDateTime i -> i.toInstant().toEpochMilli() }
Year | Year.of(2021) | [pattern: "yyyy"] | { Year y -> y }
type | timeUnit | value | settings | resolver | expected
Instant | NumericTimeUnit.SECONDS | "1640995200" | [pattern: "yyyy-MM-dd'T'HH:mm:ss.SSSZ", timezone: "UTC"] | { Instant i -> i.getEpochSecond() } | 1640995200
Date | NumericTimeUnit.MILLISECONDS | "1640995200000" | [pattern: "yyyy-MM-dd'T'HH:mm:ss.SSSZ", timezone: "UTC"] | { Date d -> d.getTime() } | 1640995200000
Timestamp | NumericTimeUnit.MILLISECONDS | "1640995200000" | [pattern: "yyyy-MM-dd'T'HH:mm:ss.SSSZ", timezone: "UTC"] | { Timestamp t -> t.getTime()} | 1640995200000
LocalDate | NumericTimeUnit.SECONDS | "19974" | [pattern: "yyyy-MM-dd", timezone: "UTC"] | { LocalDate d -> d.toString() } | "2024-09-08"
LocalDateTime | NumericTimeUnit.SECONDS | "\"2024-10-18T23:06:24.722\"" | [pattern: "yyyy-MM-dd'T'HH:mm:ss.SSS", timezone: "UTC"] | { LocalDateTime t -> t.atZone(ZoneId.of("UTC")).toInstant().toString() } | "2024-10-18T23:06:24.722Z"
ZonedDateTime | NumericTimeUnit.SECONDS | "1640995200" | [pattern: "yyyy-MM-dd'T'HH:mm:ss.SSSZ", timezone: "UTC"] | { ZonedDateTime t -> t.toString() } | "2022-01-01T00:00Z"
OffsetDateTime | NumericTimeUnit.SECONDS | "1640995200" | [pattern: "yyyy-MM-dd'T'HH:mm:ss.SSSZ", timezone: "UTC"] | { OffsetDateTime t -> t.toString() } | "2022-01-01T00:00Z"
Year | NumericTimeUnit.SECONDS | "2024" | [pattern: "yyyy", timezone: "UTC"] | { Year y -> y.toString() } | "2024"
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,12 @@ class JsonCompileSpec extends AbstractTypeElementSpec implements JsonSpec {
}

ApplicationContext buildContext(String className, @Language("java") String source, Map<String, Object> properties) {
return buildContext(className, source, properties, [:])
}

ApplicationContext buildContext(String className, @Language("java") String source, Map<String, Object> properties, Map contextProperties) {
ApplicationContext context =
buildContext(className, source, true)
buildContext(className, source, true, contextProperties)

jsonMapper = context.getBean(ObjectMapper)

Expand All @@ -51,6 +55,12 @@ class JsonCompileSpec extends AbstractTypeElementSpec implements JsonSpec {
return Argument.of(context.classLoader.loadClass(name))
}

ApplicationContext buildContext(String className, @Language("java") String cls, boolean includeAllBeans, Map contextProperties) {
def context = super.buildContext(className, cls, true, contextProperties)
jsonMapper = context.getBean(JsonMapper)
return context
}

@Override
ApplicationContext buildContext(@Language("java") String source) {
ApplicationContext context =
Expand All @@ -63,9 +73,7 @@ class JsonCompileSpec extends AbstractTypeElementSpec implements JsonSpec {

@Override
ApplicationContext buildContext(String className, @Language("java") String cls, boolean includeAllBeans) {
def context = super.buildContext(className, cls, true)
jsonMapper = context.getBean(JsonMapper)
return context
return buildContext(className, cls, includeAllBeans, [:])
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
package io.micronaut.serde.jackson.annotation


import io.micronaut.serde.jackson.JsonCompileSpec
import io.micronaut.serde.jackson.JsonFormatSpec
import spock.lang.Unroll

import java.sql.Timestamp
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.OffsetDateTime
import java.time.Year
import java.time.ZoneOffset
import java.time.ZonedDateTime

class JsonFormatSpec extends JsonCompileSpec {
class SerdeJsonFormatSpec extends JsonFormatSpec {

void "test disable validation"() {
when:
Expand Down Expand Up @@ -191,11 +183,10 @@ class Test {
BigDecimal | new BigDecimal("100000.12") | [pattern: '$###,###.###'] | '{"value":"$100,000.12"}'
BigInteger | new BigInteger("100000") | [pattern: '$###,###.###'] | '{"value":"$100,000"}'
BigInteger | new BigInteger("100000") | [pattern: '$###,###.###'] | '{"value":"$100,000"}'

}

@Unroll
void "test json format for date #type and settings #settings"() {
void "INSTANT + SQL DATE test json format for date #type and settings #settings"() {
given:
def context = buildContext('test.Test', """
package test;
Expand Down Expand Up @@ -230,15 +221,7 @@ class Test {
where:
type | value | settings | resolver
Instant | Instant.now() | [pattern: "yyyy-MM-dd'T'HH:mm:ss.SSSZ"] | { Instant i -> i.toEpochMilli() }
Date | new Date() | [pattern: "yyyy-MM-dd'T'HH:mm:ss.SSSZ"] | { Date d -> d.time }
java.sql.Date | new java.sql.Date(2021, 9, 15) | [pattern: "yyyy-MM-dd"] | { java.sql.Date d -> d }
Timestamp | new Timestamp(System.currentTimeMillis()) | [pattern: "yyyy-MM-dd'T'HH:mm:ss.SSSZ"] | { Timestamp d -> d }
LocalTime | LocalTime.now() | [pattern: "HH:mm:ss"] | { LocalTime i -> i.toSecondOfDay() }
LocalDate | LocalDate.now() | [pattern: "yyyy-MM-dd"] | { LocalDate d -> d }
LocalDateTime | LocalDateTime.now() | [pattern: "yyyy-MM-dd'T'HH:mm:ss.SSS"] | { LocalDateTime i -> i.toInstant(ZoneOffset.from(ZoneOffset.UTC)).toEpochMilli() }
ZonedDateTime | ZonedDateTime.now() | [pattern: "yyyy-MM-dd'T'HH:mm:ss.SSSZ"] | { ZonedDateTime i -> i.toInstant().toEpochMilli() }
OffsetDateTime | OffsetDateTime.now() | [pattern: "yyyy-MM-dd'T'HH:mm:ss.SSSZ"] | { OffsetDateTime i -> i.toInstant().toEpochMilli() }
Year | Year.of(2021) | [pattern: "yyyy"] | { Year y -> y }
}

}
Loading
Loading