Preface
Today while I was heads-down working, a coworker complained that when calling via OpenFeign, MVC parameter binding fails to deserialize values of type Instant. He tried all kinds of approaches and still couldn’t get it to parse. I took his demo and tried it myself—yep, it really didn’t work—so I spent the whole night tinkering… Later I found out his object wrapped a String, and that String was actually JSON he serialized himself for a “generic type” use case. Then downstream used a different parsing/serialization approach, and boom—error.
After stepping out of the project and testing on my own, I realized MVC parameter binding can support Instant serialization and deserialization….. It’s just that every JSON framework handles Instant differently, so compatibility is a bit worse. Since I’d already spent the whole night on it, it made me even more curious to experiment and see how the mainstream JSON frameworks handle Instant and what their compatibility looks like—partly to avoid future pitfalls, partly just curiosity.
If you’re interested, or you’ve run into errors like the following, I think you’ll find a direction after reading this post. The code for this article is here
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `java.time.Instant` (no Creators, like default constructor, exist)
java.lang.NoSuchMethodError:
com.fasterxml.jackson.databind.DeserializationContext.extractScalarFromObject
Caused by: java.lang.UnsupportedOperationException
at com.alibaba.fastjson.parser.deserializer.Jdk8DateCodec.deserialze
java.lang.IllegalArgumentException:
The HTTP header line [{*}] does not conform to RFC 7230 and has been ignored.
Instant and the Serialization Tools in the Ring
Instant
Instant is a Java 8 class that represents a high-precision timestamp. Fundamentally it’s not much different from System.currentTimeMillis(). Compared to the long returned by System.currentTimeMillis(), it just adds higher precision in nanoseconds. Since InfluxDB’s time primary key needs Instant, the project uses Instant as the time type.
Because its precision is high and it’s a Java 8 feature (even though Java 8 isn’t exactly new anymore), support across libraries still isn’t fully consistent.
Three JSON tools
I’m mainly looking at two JSON serialization tools. One is Jackson—no need to say much, it’s the default serializer inside Spring MVC. The other is Fastjson—also no need to say much, I’m sure everyone uses it a lot. The third is Hutool’s JSON serialization, because I’m a heavy Hutool user. I always feel that when writing code gets painful, Hutool gives me a little bit of sweetness. Since we’re testing compatibility, let’s bring Hutool into the fight too.
Prep Work
- First create a project, then create some class files. Here’s how I did it:
- Main/ (subpackages omitted)
- controller - put a controller here to test MVC parameter binding, plus a
request.httprequest file - entity - put a test entity class used specifically for passing data
- jsonconfig - JSON configs that might be needed
- controller - put a controller here to test MVC parameter binding, plus a
- test/ (subpackages omitted)
- FastJsonTests.java - Fastjson serialization/deserialization code
- JacksonTests.java - Jackson serialization/deserialization code
- MixTests.java - mixed Fastjson + Jackson serialization/deserialization code
- SummaryTests.java - summarize and print the
Instantformats serialized by each tool
- Maven dependencies
<dependencies>
<!-- 引入web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.78</version>
</dependency>
</dependencies>
- Write a test DTO
// 这里我使用了lombok来简化代码
@Data
@SuperBuilder
@NoArgsConstructor
public class TimeDTO {
/**
* 充数字段
*/
private String name;
/**
* 重点测试字段
*/
private Instant instant;
}
- To reduce duplicated code, I made all the test classes extend a base class containing the following code. Subclasses can just call it directly.
/**
* 固定Instant实例
*/
protected final Instant instant = Instant.now();
/**
* jackson的类全局使用
*/
protected final ObjectMapper objectMapper = new ObjectMapper();
FastJson Tests
There’s only one unit test here: check whether Fastjson can deserialize what it serialized itself.
@Test
void allFastJsonTest() {
TimeDTO timeDTO = TimeDTO.builder().
name("fastJson测试").
instant(instant).build();
// fastjson序列化(序列化好看一点, 然后打印出来)
String json = JSON.toJSONString(timeDTO, SerializerFeature.PrettyFormat);
System.out.println(json);
// 然后在使用fastjson反序列化
TimeDTO obj = JSON.parseObject(json, TimeDTO.class);
System.out.println(obj);
}
The result is obvious: it works.
{
"instant":"2021-10-30T08:00:03.210Z",
"name":"fastJson测试"
}
TimeDTO(name=fastJson测试, instant=2021-10-30T08:00:03.210Z)
And we can see that in the serialized JSON, the Instant format becomes a UTC time with a Z suffix.
Jackson Tests
There are more Jackson tests, because Jackson’s default serialization differs from serialization after registering the time module, so I tested them separately.
The time module is a class under Jackson’s jsr310 package. Spring usually already brings it in, so you don’t need to add extra dependencies. When using Jackson, you just need to register it via objectMapper.registerModule(new JavaTimeModule());. Basically it adds a bunch of deserializers for you. If you look into the source, the no-arg constructor goes crazy adding all kinds of time deserializers.

jackson (no time module) To jackson (no time module)
@Test
@SneakyThrows
void allJacksonWithNonJava8Test() {
TimeDTO timeDTO = TimeDTO.builder().
name("jackson不注册时间模块序列化").
instant(instant).build();
// jackson序列化(序列化好看一点, 然后打印出来)
String json = objectMapper.writer().withDefaultPrettyPrinter().writeValueAsString(timeDTO);
System.out.println(json);
// 然后再使用jackson反序列化
TimeDTO obj = objectMapper.readValue(json, TimeDTO.class);
System.out.println(obj);
}
{
"name" : "jackson不注册时间模块序列化",
"instant" : {
"epochSecond" : 1635582033,
"nano" : 590000000
}
}
!!!报错
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `java.time.Instant` (no Creators, like default constructor, exist)
Nice—straight up error. And you can see that without the time module, Jackson serializes Instant as an object, and inside it there’s another JSON structure.
jackson (time module registered) To jackson (time module registered)
@Test
@SneakyThrows
void allJacksonWithJava8Test() {
TimeDTO timeDTO = TimeDTO.builder().
name("jackson注册时间模块序列化").
instant(instant).build();
// 给全局变量的objectMapper注册一下时间模块
objectMapper.registerModule(new JavaTimeModule());
// jackson序列化(序列化好看一点, 然后打印出来)
String json = objectMapper.writer().withDefaultPrettyPrinter().writeValueAsString(timeDTO);
System.out.println(json);
// 然后再使用jackson反序列化
TimeDTO obj = objectMapper.readValue(json, TimeDTO.class);
System.out.println(obj);
}
result:
{
"name" : "jackson注册时间模块序列化",
"instant" : 1635582344.024000000
}
TimeDTO(name=jackson注册时间模块序列化, instant=2021-10-30T08:25:44.024Z)
With the time module registered, Jackson can serialize Instant normally. And if you look closely at the serialized JSON, it’s a floating-point number: the integer part is seconds, and the fractional part is nanoseconds.
jackson (time module registered) To jackson (no time module)
@Test
@SneakyThrows
void JacksonWithJava8ToJacksonWithNonJava8Test() {
TimeDTO timeDTO = TimeDTO.builder().
name("jackson注册时间模块序列化, 然后用没有注册时间模块的jackson反序列化").
instant(instant).build();
// 给全局变量的objectMapper注册一下时间模块
objectMapper.registerModule(new JavaTimeModule());
// jackson序列化(序列化好看一点, 然后打印出来)
String json = objectMapper.writer().withDefaultPrettyPrinter().writeValueAsString(timeDTO);
System.out.println(json);
// 然后再使用重新创建一个jackson反序列化(和这个是没有注册时间模块的)
TimeDTO obj = new ObjectMapper().readValue(json, TimeDTO.class);
System.out.println(obj);
}
{
"name" : "jackson注册时间模块序列化, 然后用没有注册时间模块的jackson反序列化",
"instant" : 1635582847.359000000
}
!!!报错
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `java.time.Instant` (no Creators, like default constructor, exist)
See? Jackson without the time module cannot parse the xxx.xxx floating-point format serialized by Jackson with the time module.
jackson (no time module) To jackson (time module registered)
@Test
@SneakyThrows
void JacksonNonWithJava8ToJacksonWithJava8Test() {
TimeDTO timeDTO = TimeDTO.builder().
name("jackson注册时间模块序列化, 然后用没有注册时间模块的jackson反序列化").
instant(instant).build();
// 用不注册时间模块的jackson序列化(序列化好看一点, 然后打印出来)
String json = objectMapper.writer().withDefaultPrettyPrinter().writeValueAsString(timeDTO);
System.out.println(json);
// 然后给全局变量的objectMapper注册一下时间模块
objectMapper.registerModule(new JavaTimeModule());
// 然后再jackson反序列化(现在已经注册时间模块的)
TimeDTO obj = objectMapper.readValue(json, TimeDTO.class);
System.out.println(obj);
}
{
"name" : "jackson注册时间模块序列化, 然后用没有注册时间模块的jackson反序列化",
"instant" : {
"epochSecond" : 1635583012,
"nano" : 867000000
}
}
!!!报错
java.lang.NoSuchMethodError:
com.fasterxml.jackson.databind.DeserializationContext.extractScalarFromObject
The serialization format produced by Jackson without the time module can’t be parsed even by Jackson with the time module. It’s this object form. In short: this object-form Instant serialized by Jackson cannot be deserialized by Jackson itself… whether you add the time module or not. Awkward.
Mixed Tests
From above, Fastjson is pretty solid—at least it can deserialize what it serialized. Jackson only works if both serialization and deserialization have the time module; if neither has it, you get the awkward situation where it can’t even deserialize its own output. Now let’s mix them up.
jackson (no time module) To FastJson
@Test
@SneakyThrows
void JackSonWithNonJava8ToFastJSON() {
TimeDTO timeDTO = TimeDTO.builder().
name("jackson不注册java8时间模块序列化, 然后用FastJson反序列化").
instant(instant).build();
// 用没用注册java8时间模块的jackson序列化
String json = objectMapper.writer().withDefaultPrettyPrinter().writeValueAsString(timeDTO);
System.out.println(json);
// 用fastjson直接反序列化
TimeDTO obj = JSON.parseObject(json, TimeDTO.class);
System.out.println(obj);
}
{
"name" : "jackson不注册java8时间模块序列化, 然后用FastJson反序列化",
"instant" : {
"epochSecond" : 1635610701,
"nano" : 940000000
}
}
TimeDTO(name=jackson不注册java8时间模块序列化, 然后用FastJson反序列化, instant=2021-10-30T16:18:21.940Z)
This is insane. Even though Jackson itself can’t deserialize this object format, Fastjson can successfully deserialize the JSON object format Instant.
jackson (time module registered) To FastJson
@Test
@SneakyThrows
void JackSonWithJava8ToFastJSON() {
TimeDTO timeDTO = TimeDTO.builder().
name("jackson注册java8时间模块序列化, 然后用FastJson反序列化").
instant(instant).build();
// 用注册java8时间模块的jackson序列化
objectMapper.registerModule(new JavaTimeModule());
String json = objectMapper.writer().withDefaultPrettyPrinter().writeValueAsString(timeDTO);
System.out.println(json);
// 用fastjson直接反序列化
TimeDTO obj = JSON.parseObject(json, TimeDTO.class);
System.out.println(obj);
}
{
"name" : "jackson注册java8时间模块序列化, 然后用FastJson反序列化",
"instant" : 1635610909.898000000
}
Caused by: java.lang.UnsupportedOperationException
at com.alibaba.fastjson.parser.deserializer.Jdk8DateCodec.deserialze
Unexpectedly, Fastjson can’t parse the Instant serialized by Jackson with the time module. In other words, Fastjson cannot parse the xxx.xxx floating-point format into an Instant.
FastJson To jackson (no time module)
@Test
@SneakyThrows
void FastJSONToJackSonWithNonJava8() {
TimeDTO timeDTO = TimeDTO.builder().
name("使用FastJson序列化, 然后使用没用注册java8时间模块的jackson反序列化").
instant(instant).build();
// 使用FastJson进行序列化
String json = JSON.toJSONString(timeDTO, SerializerFeature.PrettyFormat);
System.out.println(json);
// 使用没用注册java8时间模块的jackson反序列化
TimeDTO obj = objectMapper.readValue(json, TimeDTO.class);
System.out.println(obj);
}
result:
{
"instant":"2021-10-30T16:27:11.054Z",
"name":"使用FastJson序列化, 然后使用没用注册java8时间模块的jackson反序列化"
}
!!!报错
com.fasterxml.jackson.databind.exc.InvalidDefinitionException:
Cannot construct instance of `java.time.Instant` (no Creators, like default constructor, exist)
It errors out. If Jackson doesn’t register the time module, it can’t parse Fastjson’s UTC format. Basically, Jackson without the time module can’t deserialize any Instant format, including its own.
FastJson To jackson (time module registered)
@Test
@SneakyThrows
void FastJSONToJackSonWithJava8() {
TimeDTO timeDTO = TimeDTO.builder().
name("使用FastJson序列化, 然后使用注册java8时间模块的jackson反序列化").
instant(instant).build();
// 使用FastJson进行序列化
String json = JSON.toJSONString(timeDTO, SerializerFeature.PrettyFormat);
System.out.println(json);
// 使用注册java8时间模块的jackson反序列化
objectMapper.registerModule(new JavaTimeModule());
TimeDTO obj = objectMapper.readValue(json, TimeDTO.class);
System.out.println(obj);
}
{
"instant":"2021-10-30T16:30:07.833Z",
"name":"使用FastJson序列化, 然后使用注册java8时间模块的jackson反序列化"
}
TimeDTO(name=使用FastJson序列化, 然后使用注册java8时间模块的jackson反序列化, instant=2021-10-30T16:30:07.833Z)
Success. So the conclusion is: Jackson with the time module can deserialize both the xxx.xxx format and the UTC Z format.
Summary (If you don’t want to read, jump here for the conclusion)
Conclusion
The examples above are a bit messy. If you run them yourself you’ll get a very clear picture. But if you just want the conclusion, look here.
First, let’s see what Instant looks like after serialization by different tools:
@Test
@SneakyThrows
void sumTest() {
TimeDTO timeDTO = TimeDTO.builder().instant(instant).build();
timeDTO.setName("FastJson序列化以后的结果");
String str1 = JSON.toJSONString(timeDTO);
timeDTO.setName("Jackson没注册时间模块序列化以后的结果");
String str2 = objectMapper.writeValueAsString(timeDTO);
timeDTO.setName("Jackson注册了时间模块序列化以后的结果");
String str3 = new ObjectMapper().registerModule(new JavaTimeModule()).writeValueAsString(timeDTO);
timeDTO.setName("HuTools工具序列化以后的结果");
String str4 = JSONUtil.toJsonStr(timeDTO);
System.out.println(str1);
System.out.println(str2);
System.out.println(str3);
System.out.println(str4);
}
{"name":"FastJson序列化以后的结果","instant":"2021-10-30T16:59:29.896Z"}
{"name":"Jackson没注册时间模块序列化以后的结果","instant":{"epochSecond":1635613169,"nano":896000000}}
{"name":"Jackson注册了时间模块序列化以后的结果","instant":1635613169.896000000}
{"name":"HuTools工具序列化以后的结果","instant":1635613169896}
You can see every library serializes Instant differently. Based on my tests, I made a table for easier viewing, including Hutool:
| Name | Serialized Instant format |
Deserialization behavior |
|---|---|---|
| FastJson | “2021-10-30T16:59:29.896Z” | FstJson can Hutool can jackson (time module not registered) cannot jackson (time module registered) can |
| Hutool | 1635613169896 | FstJson can Hutool can jackson (time module not registered) cannot jackson (time module registered) cannot (no error, but it parses milliseconds as seconds) |
| Jackson (time module not registered) | {“epochSecond”:1635613169,”nano”:896000000} | FstJson can Hutool cannot (no error, but becomes null) jackson (time module not registered) cannot jackson (time module registered) cannot |
| Jackson (time module registered) | 1635613169.896000000 | FstJson cannot Hutool cannot (no error, but becomes null) jackson (time module not registered) cannot jackson (time module registered) can |
From the table, the bolded Jackson without the time module is just… too weak. It can’t handle any of them. On the other hand, even though Fastjson gets dissed every day, its compatibility is actually pretty strong. And Fastjson’s serialization/deserialization is customizable (Jackson is too), but in projects Fastjson often feels more convenient. So below I’ll write a custom deserializer to let Fastjson perfectly fill in this missing support.
Enhancing Fastjson
Just extend ObjectDeserializer and override deserialze. Focus on the two parameters: parser is where you extract the object you want to deserialize (note: you can only extract once). The other is name—even though it’s typed as Object, it’s actually the parser key. Extract it, cast to string, then parse it. For the xxx.xxx format, the part before the dot is seconds, and the part after is nanoseconds. Use Instant’s static methods to split it and create an Instant, then return it.
public class InstantDeserialize implements ObjectDeserializer {
@Override
@SuppressWarnings("unchecked")
public Instant deserialze(DefaultJSONParser parser, Type type, Object name) {
// 参数在parser里面, name是参数名字(虽然用object接收, 其实是字符串)
Object value = parser.parse(name);
// 通过'.'分割, 然后拿到list
List<String> split = StrUtil.split(Convert.toStr(value), '.');
// 把前部分变成秒, 后部分变成纳秒, 然后生成Instant返回. 如果发生异常 返回一个null
return Try.of(() -> Instant.ofEpochSecond(Convert.toInt(split.get(0)), Convert.toInt(split.get(1)))).getOrNull();
}
@Override
public int getFastMatchToken() {
return 0;
}
}
After writing the deserializer, you don’t need to add beans or anything, because we don’t need a global setting. Just configure it on the field that needs it in the class.
@Data
@SuperBuilder
@NoArgsConstructor
public class TimeDTO {
/**
* 充数字段
*/
private String name;
/**
* 重点测试字段
*/
@JSONField(deserializeUsing = InstantDeserialize.class)
private Instant instant;
}
Now you can parse the xxx.xxx format.
Finally: Test MVC Parameter Binding for Request Parameters
Spring Boot’s parameter binding serialization/deserialization uses Jackson by default.

So the question is: does the Jackson used by MVC parameter binding register the time module? Actually from the screenshot above you can already see the jsr310 dependency is there, so it’s very likely registered. Show code.
In the main package (the test cases above were all under test), I wrote a controller, and there’s a .http request sample in the same package.
@RestController
public class TestController {
/**
* 接口测试样例请看同包下.http文件
* @param timeDTO 测试实体类
* @return 测试返回数据
*/
@PostMapping("/test")
public ResponseEntity<TimeDTO> parameterBindingTest(@RequestBody TimeDTO timeDTO) {
return ResponseEntity.ok(timeDTO);
}
}

The result matches our experiments: sending "instant":"2021-10-30T16:59:29.896Z" and "instant":"2021-10-30T16:59:29.896Z" can be parsed normally; sending other formats either throws an error or parses incorrectly.
Of course, you can also switch Spring MVC’s default serializer to Fastjson via configuration—just put this method into your Fastjson config class.
/**
* 序列化机制改为fastJson
* @return
*/
@Bean
@Primary
public HttpMessageConverters fastJsonHttpMessageConverters() {
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(
SerializerFeature.DisableCircularReferenceDetect,
SerializerFeature.WriteBigDecimalAsPlain
);
fastConverter.setFastJsonConfig(fastJsonConfig);
List<MediaType> supportedMediaTypes = new ArrayList<>();
supportedMediaTypes.add(MediaType.APPLICATION_JSON);
fastConverter.setSupportedMediaTypes(supportedMediaTypes);
return new HttpMessageConverters(fastConverter);
}
Afterword
This ended up being a bit long. I’m not sure how to write it in a more structured way. Besides the examples, I also briefly mentioned Fastjson custom deserialization and replacing MVC’s default serialization config—there are actually a lot of details inside. If there are cases I didn’t cover in the article, the code has already been uploaded to Github. If you’re interested, you can clone it and run it yourself. The code comments are very detailed and the examples are quite complete—except the code style is Google’s 224 format, which feels a bit awkward to me (company-required style; I didn’t change it back for my own project).
All articles in this blog, unless otherwise stated, are licensed under @Oreoft . Please indicate the source when reprinting!