Skip to content

Commit

Permalink
Bug Fix: Failed to deserialize the date field from a number using the @…
Browse files Browse the repository at this point in the history
…jsonformat annotation (#927)
  • Loading branch information
jjhz authored Oct 29, 2024
1 parent f8651b3 commit 1a5931e
Show file tree
Hide file tree
Showing 7 changed files with 147 additions and 231 deletions.
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

0 comments on commit 1a5931e

Please sign in to comment.