在将 json 反序列化为对象时,使用 jackson 将 asp.net / MS 专有 json Dateformat 转换为 java8 LocalDateTime

Posted

技术标签:

【中文标题】在将 json 反序列化为对象时,使用 jackson 将 asp.net / MS 专有 json Dateformat 转换为 java8 LocalDateTime【英文标题】:Convert asp.net / MS proprietary json Dateformat to java8 LocalDateTime with jackson while deserializing json to object 【发布时间】:2017-11-09 23:53:52 【问题描述】:

我从 Spring Boot 应用程序调用 web 服务,使用 jackson-jsr-310 作为 maven 依赖项,以便能够使用 LocalDateTime

RestTemplate restTemplate = new RestTemplate();
HttpHeaders httpHeaders = this.createHeaders();
ResponseEntity<String> response;
response  = restTemplate.exchange(uri,HttpMethod.GET,new HttpEntity<Object>(httpHeaders),String.class);
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.registerModule(new JavaTimeModule());
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
BusinessPartner test = mapper.readValue(response.getBody(), BusinessPartner.class);

我的问题在最后一行,代码产生了这个错误:

java.time.format.DateTimeParseException:无法在索引 0 处解析文本“/Date(591321600000)/”

response.getBody() 中生成的 JSON 如下所示:

  
    "d":  
        ...
        "Address":...,
        "FirstName":"asd",
        "LastName":"asd",
        "BirthDate":"\/Date(591321600000)\/",
    

在我的模型类中,我有以下成员:

@JsonProperty("BirthDate")
private LocalDateTime birthDate;

所以,在这里搜索了一下后,我发现/Date(...)/ 似乎是 Microsoft 专有的日期格式,Jackson 默认无法将其反序列化为对象。

一些问题建议创建一个自定义 SimpleDateFormat 并将其应用于我尝试做的 opbject 映射器,但后来我想我错过了 mapper.setDateFormat(new SimpleDateFormat("...")); 的正确语法

我试过用例如mapper.setDateFormat(new SimpleDateFormat("/Date(S)/"));

或者最后甚至mapper.setDateFormat(new SimpleDateFormat("SSSSSSSSSSSS)"));

但这似乎也不起作用,所以我现在没有想法,希望这里的一些人可以帮助我。

编辑 1:

进一步调查,似乎一种方法是为杰克逊编写自定义DateDeSerializer。所以我尝试了这个:

@Component
public class JsonDateTimeDeserializer extends JsonDeserializer<LocalDateTime> 

private DateTimeFormatter formatter;

private JsonDateTimeDeserializer() 
    this(DateTimeFormatter.ISO_LOCAL_DATE_TIME);


public JsonDateTimeDeserializer(DateTimeFormatter formatter) 
    this.formatter = formatter;


@Override
public LocalDateTime deserialize(JsonParser parser, DeserializationContext context) throws IOException

    if (parser.hasTokenId(JsonTokenId.ID_STRING)) 
        String unixEpochString = parser.getText().trim();
        unixEpochString = unixEpochString.replaceAll("[^\\d.]", "");

        long unixTime = Long.valueOf(unixEpochString);
        if (unixEpochString.length() == 0) 
            return null;
        

        LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(unixTime), ZoneId.systemDefault());
        localDateTime.format(formatter);

        return localDateTime;
    
    return null;

实际上几乎返回了我想要的,使用

在模型中注释我的字段
@JsonDeserialize(using = JsonDateTimeDeserializer.class)

但不完全是: 此代码返回 LocalDateTime 值:1988-09-27T01:00。 但在第三方系统中,xml值为1988-09-27T00:00:00

很明显,这里的ZoneId:

LocalDateTime localDateTime = LocalDateTime.ofInstant(Instant.ofEpochMilli(unixTime), ZoneId.systemDefault());

是问题所在,除了日期格式错误。

那么这里有人可以帮我解决如何切换到time-part 始终使用零并让我的日期格式正确吗?会很棒!

【问题讨论】:

591321600000 是纪元毫秒(从 1970-01-01T00:00:00Z 开始的毫秒数)? @Hugo 是的,是的。请查看我的编辑以获取更多信息。 System.out.println(ZoneId.systemDefault()) 的输出是什么? 这是欧洲/柏林(这里下雨),偏移量为 +01:00,这就是为什么我说问题出在哪里很明显。没想到来自圣保罗的人 ;) 【参考方案1】:

这是我编写的一些 Groovy 代码,它也处理时区偏移:https://gist.github.com/jeffsheets/938733963c03208afd74927fb6130884

class JsonDotNetLocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> 

    @Override
    LocalDateTime deserialize(JsonParser parser, DeserializationContext ctxt) 
        convertDotNetDateToJava(parser.text.trim())
    

    /**
     * Returns a Java LocalDateTime when given a .Net Date String
     * /Date(1535491858840-0500)/
     */
    static LocalDateTime convertDotNetDateToJava(String dotNetDate) 
        // Strip the prefix and suffix to just 1535491858840-0500
        String epochAndOffset = dotNetDate[6..-3]

        // 1535491858840
        String epoch = epochAndOffset[0..-6]

        // -0500 Note, keep the negative/positive indicator
        String offset = epochAndOffset[-5..-1]
        ZoneId zoneId = ZoneId.of("UTC$offset")

        LocalDateTime.ofInstant(Instant.ofEpochMilli(epoch.toLong()), zoneId)
    

【讨论】:

【参考方案2】:

我假设数字 591321600000 是纪元毫秒(1970-01-01T00:00:00Z 的毫秒数)。

如果是这样的话,我认为SimpleDateFormat 帮不了你(至少我找不到使用这个类从 epoch milli 解析日期的方法)。模式S(根据javadoc)用于格式化或解析时间的毫秒字段(因此其最大值为999),不适用于您的情况。

我可以让它工作的唯一方法是创建一个自定义反序列化器。

首先,我创建了这个类:

public class SimpleDateTest 

    @JsonProperty("BirthDate")
    private LocalDateTime birthDate;

    // getter and setter

然后我创建了自定义反序列化器并将其添加到自定义模块中:

// I'll explain all the details below
public class CustomDateDeserializer extends JsonDeserializer<LocalDateTime> 

    @Override
    public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException 
        String s = p.getText(); // s is "/Date(591321600000)/"

        // assuming the format is always /Date(number)/
        long millis = Long.parseLong(s.replaceAll("\\/Date\\((\\d+)\\)\\/", "$1"));

        Instant instant = Instant.ofEpochMilli(millis); // 1988-09-27T00:00:00Z

        // instant is in UTC (no timezone assigned to it)
        // to get the local datetime, you must provide a timezone
        // I'm just using system's default, but you must use whatever timezone your system uses
        return instant.atZone(ZoneId.systemDefault()).toLocalDateTime();
    


public class CustomDateModule extends SimpleModule 

    public CustomDateModule() 
        addDeserializer(LocalDateTime.class, new CustomDateDeserializer());
    

然后我将此模块添加到我的映射器中并且它起作用了:

// using reduced JSON with only the relevant field
String json = " \"BirthDate\": \"\\/Date(591321600000)\\/\" ";
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
// add my custom module
mapper.registerModule(new CustomDateModule());

SimpleDateTest value = mapper.readValue(json, SimpleDateTest.class);
System.out.println(value.getBirthDate()); // 1988-09-26T21:00

现在一些关于反序列化方法的 cmets。

首先,我将millis 591321600000 转换为Instant(代表UTC 时刻的类)。以毫秒为单位的591321600000 等价于1988-09-27T00:00:00Z

但这是 UTC 日期/时间。要获取 本地 日期和时间,您必须知道您所在的时区,因为在每个时区它的日期和时间都不同(世界上的每个人都在同一时刻,但他们的本地日期/时间可能会有所不同,具体取决于他们所在的位置)。

在我的示例中,我只使用了ZoneId.systemDefault(),它获取了我系统的默认时区。但是,如果您不想依赖默认值并希望使用特定时区,请使用 ZoneId.of("timezone name") 方法(您可以使用 ZoneId.getAvailableZoneIds() 获取所有可用时区名称的列表 - 此方法返回所有有效名称接受ZoneId.of() 方法)。

由于我的默认时区是America/Sao_Paulo,因此此代码将birthDate 设置为1988-09-26T21:00

如果您不想转换到特定时区,可以使用ZoneOffset.UTC。因此,在反序列化方法中,最后一行将是:

   return instant.atZone(ZoneOffset.UTC).toLocalDateTime();

现在本地日期将是 1988-09-27T00:00 - 因为我们使用的是 UTC 偏移量,所以没有时区转换并且本地日期/时间没有改变。


PS:如果您需要将birthDate转换回MS的自定义格式,您可以编写自定义序列化程序并添加到自定义模块中。要将LocalDateTime 转换为该格式,您可以:

LocalDateTime birthDate = value.getBirthDate();
// you must know in what zone you are to convert it to epoch milli (using default as an example)
Instant instant = birthDate.atZone(ZoneId.systemDefault()).toInstant();
String msFormat = "/Date(" + instant.toEpochMilli() + ")/";
System.out.println(msFormat); // /Date(591321600000)/

请注意,要将LocalDateTime 转换为Instant,您必须知道您所在的时区。在这种情况下,我建议使用相同的时区进行序列化和反序列化(在您的情况下,您可以使用ZoneOffset.UTC 而不是ZoneId.systemDefault()

【讨论】:

实际上,这与我得到的位置非常接近 :) 我会接受你的回答,因为这实际上解决了 95%。不过,如果你能帮助我解决我编辑中的最后一个问题,那就太好了! 发生小时变化是因为您将 UTC 值(即时)转换为您的时区。 是的,但可以不这样做吗?据我所知,必须提供一个时区。 是的,看我的编辑:只需使用ZoneOffset.UTC 而不是ZoneId.systemDefault() 非常感谢....我唯一的问题是我收到了 Date(1443630600000+0200) 并且我必须以相同的格式发送。可以这样做吗?

以上是关于在将 json 反序列化为对象时,使用 jackson 将 asp.net / MS 专有 json Dateformat 转换为 java8 LocalDateTime的主要内容,如果未能解决你的问题,请参考以下文章

在将实体框架对象图序列化为 Json 时防止 ***Exception

如何在将 xml 反序列化为 c# 对象时获取单个 xml 元素的多个值?

使用 GSON 将 JSON 反序列化为 Java 对象时遇到问题

在 C# 中将 JSON 数组反序列化为对象

反序列化时如何从json中删除k__BackingField

无法将 JSON 字符串反序列化为 C# 对象