Meu JSON retornou a data /Date(1555375534143-0300)/, o que eu faço?

Meu JSON retornou a data /Date(1555375534143-0300)/, o que eu faço?

Às vezes aparece uma data retornada como "/Date(1555375534143-0300)/". Este é um dos piores formatos de data já criados, mas não é impossível de interpretá-lo, vamos lá?


Este formato é conhecido como Microsoft JSON Date, já que algumas versões mais antigas do Framework .NET serializavam datas neste formato (embora não pareça ser um “nome oficial”, mas muitos o conhecem assim).

É importante salientar que ele não faz parte da especificação oficial do formato JSON. Conforme já explicado neste outro post, um JSON possui vários tipos definidos, como números e strings, mas não possui nenhum tipo específico para datas. Portanto, o valor em questão (/Date(1555375534143-0300)/) é na verdade uma string. Cabe a você transformar esta string em uma data.

Como interpretar este formato

Este valor pode vir como /Date(1555375534143-0300)/ ou /Date(1555375534143)/. As barras, parênteses e a palavra “Date” são sempre fixas, e o que importa de fato é o que está dentro dos parênteses.

O número gigante (1555375534143) é um timestamp Neste link há uma descrição bem detalhada (recomendo a leitura caso você não saiba o que é um timestamp), mas apenas para resumir:

O segundo ponto é importante, pois para converter o timestamp para uma data, você precisa saber qual timezone (fuso horário) será usado, já que em cada timezone o resultado será uma data e hora diferente (você leu o link que eu sugeri, né? Lá explica isso em detalhes). Por exemplo, o timestamp 1555375534143 corresponde às seguintes datas e horas:

Data e hora Fuso horário
15/04/2019, às 21:45:34.143 São Paulo
15/04/2019, às 17:45:34.143 Los Angeles
16/04/2019, às 09:45:34.143 Tóquio
16/04/2019, às 00:45:34.143 UTC

Todas as datas e horas acima correspondem ao mesmo timestamp (1555375534143), portanto, ao fazer a conversão é necessário saber qual o timezone sendo usado.

O segundo valor (-0300) é um offset, ou seja, a diferença em relação a UTC. No caso, -0300 significa 3 horas a menos que o UTC, que é o offset usado em São Paulo naquele instante.

Dito isso, como interpretar valores como /Date(1555375534143-0300)/ ou /Date(1555375534143)/? Depende.

Você pode pegar somente o timestamp e ignorar o offset, e em seguida converter o timestamp para uma data e hora, usando algum timezone qualquer. Ou você pode usar o offset que foi passado para fazer esta conversão (e usar UTC, ou algum timezone default, caso não haja um offset). Ou você pode simplesmente usar o valor do timestamp (1555375534143) da forma que está. Tudo depende do que você quer ou precisa fazer.

Para extrair os valores, você pode usar tanto substring quanto expressões regulares (regex), e dependendo da linguagem, é possível fazer o parsing diretamente, usando uma API de datas. Abaixo tem exemplos em algumas das linguagens que conheço (com uma ênfase maior em Java, pois é a API que tenho mais familiaridade). Se quiser, pode usar os links abaixo para ir direto para a linguagem de sua preferência:

Java

Se você estiver usando o Java >= 8, use a API java.time. Com um java.time.format.DateTimeFormatterBuilder, é possível construir um java.time.format.DateTimeFormatter que faz o parsing deste formato.

DateTimeFormatter parser = new DateTimeFormatterBuilder()
    // parte inicial
    .appendLiteral("/Date(")
    // para o timestamp, usa-se o InstantSeconds e os milissegundos
    .appendValue(ChronoField.INSTANT_SECONDS)
    .appendValue(ChronoField.MILLI_OF_SECOND, 3)
    // offset opcional (colchetes indicam que o campo é opcional)
    .appendPattern("[XX]")
    // se não tiver offset, assume-se que é zero (UTC)
    .parseDefaulting(ChronoField.OFFSET_SECONDS, 0)
    // parte final, e cria o DateTimeFormatter
    .appendLiteral(")/").toFormatter();
OffsetDateTime odt1 = OffsetDateTime.parse("/Date(1555375534143)/", parser);
System.out.println(odt1); // 2019-04-16T00:45:34.143Z
OffsetDateTime odt2 = OffsetDateTime.parse("/Date(1555375534143-0300)/", parser);
System.out.println(odt2); // 2019-04-15T21:45:34.143-03:00

No exemplo acima eu uso java.time.temporal.ChronoField para definir o trecho que obtém o timestamp. São usados INSTANT_SECONDS, que captura o trecho 1555375534, e MILLI_OF_SECONDS, que captura o trecho 143.

Depois, é feito o parsing para um java.time.OffsetDateTime, que possui a data, hora e offset. Quando a entrada possui um offset, eu uso o respectivo valor, mas quando não tem offset, por padrão eu uso zero (que corresponde a UTC).

Com isso, eu tenho o valor do timestamp e o offset, e internamente o método parse faz as devidas conversões para gerar a data e hora correspondentes. Repare que isso faz com que os valores de data e hora mudem no resultado final, por isso é importante definir qual offset ou timezone será usado.

Se quiser o valor numérico do timestamp, use odt1.toInstant().toEpochMilli() (também funciona com odt2, pois ambos correspondem ao mesmo instante - e portanto, ao mesmo timestamp). Já para obter o offset, você pode usar:

ZoneOffset offset = odt2.getOffset();
System.out.println(offset.getTotalSeconds()); // -10800
System.out.println(Duration.ofSeconds(offset.getTotalSeconds()).toHours()); // -3

Neste caso, há diferença em usar odt1 ou odt2, pois o primeiro está em UTC (offset zero) e o segundo está no offset -03:00 (3 horas a menos que UTC). O método getTotalSeconds() retorna o valor total do offset em segundos (no caso acima, -10800, que corresponde a “menos 3 horas”).

Também é usado um java.time.Duration para converter o valor dos segundos para horas, resultando em -3. Mas atenção, o método toHours() arredonda o valor e isso pode fazer diferença para casos em que o offset não é de horas inteiras (como ocorre por exemplo na Índia, que atualmente usa o offset +05:30 - 5 horas e meia à frente do UTC). Para obter o valor exato do offset, melhor usar getTotalSeconds().

Lembrando que, como o offset não é o mesmo, odt1.getOffset().getTotalSeconds() retornará zero, já que ele foi obtido da string /Date(1555375534143)/, que não possui offset, e no nosso DateTimeFormatter foi definido que neste caso usa-se o offset zero.

Se você estiver usando Java 6 e 7, pode usar o Threeten Backport, um backport do java.time. Ele basicamente possui as mesmas classes e métodos do java.time, a diferença é que o nome do pacote é org.threeten.bp. Ou seja, com exceção dos import’s, o código ficará igual ao do exemplo acima.


Obviamente, você também pode usar a API legada (java.util.Date e java.text.SimpleDateFormat). Infelizmente, com esta API não é possível fazer algo tão direto quanto o exemplo acima com DateTimeFormatter, então o jeito é extrair as informações diretamente da String. Uma das opções é usar regex, através do pacote java.util.regex:

// Matcher e Pattern fazem parte do pacote java.util.regex
Matcher matcher = Pattern.compile("/Date\\((\\d+)([-+]\\d{4})?\\)/")
                         .matcher("/Date(1555375534143)/");
if (matcher.find()) {
    long timestamp = Long.parseLong(matcher.group(1));
    String offset = matcher.group(2);
    if (offset == null) { // se não tem offset, usa zero
        offset = "+0000";
    }

    // se quiser um java.util.Date, pode parar por aqui
    Date date = new Date(timestamp);

    // se quiser obter uma String com a data e hora correspondente no offset em questão
    SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy HH:mm:ss XXX");
    // usar o offset para converter o Date em uma data e hora
    sdf.setTimeZone(TimeZone.getTimeZone("GMT" + offset));
    System.out.println(sdf.format(date)); // 16/04/2019 00:45:34 Z
} else {
    System.out.println("String não está no formato correto");
}

A lógica geral é a mesma: tenta-se obter o timestamp e o offset da String. Caso o offset não esteja presente, algum valor default é usado (no caso, estou usando zero). Eu uso \\d+ para capturar o timestamp (um ou mais dígitos), e para o offset eu uso [-+] (o sinal de menos ou de mais) seguido de \\d{4} (quatro dígitos), e uso o ? para dizer que o offset é opcional. Cada um dos campos está entre parênteses para formar grupos de captura, e com isso eu posso obter os respectivos valores usando o método group (no caso, o grupo 1 é o timestamp e o grupo 2 é o offset).

A seguir, eu uso o timestamp para criar um java.util.Date. E um Date, apesar do nome, não representa uma data, e sim um timestamp. Esse é um ponto meio confuso, pois ao imprimir o Date, ele usa o timezone default da JVM para obter os valores de data e hora. Exemplo:

Date data = new Date(1555375534143L);
TimeZone.setDefault(TimeZone.getTimeZone("America/Sao_Paulo"));
System.out.println(data.getTime() + "=" + data);
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Tokyo"));
System.out.println(data.getTime() + "=" + data);

No código acima estou imprimindo o timestamp (através do método getTime()), e em seguida imprimo a própria data, que internamente chama o método toString(). A saída é:

1555375534143=Mon Apr 15 21:45:34 BRT 2019
1555375534143=Tue Apr 16 09:45:34 JST 2019

Repare que o valor do timestamp é o mesmo, mas o valor retornado por toString() não, pois este método usa o timezone default que está setado no momento, para converter o timestamp para uma data e hora específicas. Mas o Date não possui esses valores de data e hora: internamente, esta classe só possui o valor do timestamp. Por isso que o construtor de Date só precisa do timestamp, e se quisermos apenas uma instância de Date, não precisamos do offset.

Mas caso você queira uma String contendo a data, hora correspondentes ao timestamp, no offset indicado, basta usar um SimpleDateFormat, como o código acima mostra. Ele também usa um java.util.TimeZone com o valor do offset que foi obtido pela regex. Com isso, a saída é uma String que corresponde ao timestamp e ao offset que estavam na entrada.

No exemplo acima, a saída é 16/04/2019 00:45:34 Z (este “Z” no final indica que está em UTC - ou seja, que o offset é zero). Se eu testar com "/Date(1555375534143-0300)/", a saída é 15/04/2019 21:45:34 -03:00.

Também daria para obter o timestamp e offset usando substring, e fazendo alguns if’s para saber se o offset existe, mas eu acho que a solução com regex é mais direta nesse caso.

C#

Em C# você pode usar o Json.NET, que possui a classe JsonConvert. Com isso podemos obter um DateTime:

string sa = @"""" + "/Date(1555375534143-0300)/" + @"""";
DateTime dt = JsonConvert.DeserializeObject<DateTime>(sa);
Console.WriteLine(dt); // 4/15/19 9:45:34 PM

De forma similar ao que foi feito acima em Java, também podemos obter o timestamp e o offset usando regex, e em seguida usamos um DateTimeOffset para obter a data e hora correspondente:

Regex r = new Regex(@"/Date\((\d+)([-+]\d{4})?\)/");
Match match = r.Match("/Date(1555375534143-0300)/");
if (match.Success)
{
    long timestamp = long.Parse(match.Groups[1].Value);
    TimeSpan offset = TimeSpan.Zero; // offset por padrão é zero
    if(match.Groups[2].Success)
    { // se foi encontrado offset
        string value = match.Groups[2].Value;
        int hours = int.Parse(value.Substring(1, 2));
        if (value.Substring(0, 1).Equals("-"))
        {
            hours = -hours;
        }
        int minutes = int.Parse(value.Substring(3));
        offset = new TimeSpan(hours, minutes, 0);
    }
    DateTimeOffset dt = DateTimeOffset.FromUnixTimeMilliseconds(timestamp).ToOffset(offset);
    Console.WriteLine(dt); // 4/15/19 9:45:34 PM -03:00
}

Foi feito um pequeno trabalho com substrings para obter o valor correto do offset como um TimeSpan. Em seguida, usamos o método FromUnixTimeMilliseconds, passando o timestamp, e o método ToOffset, que converte para o offset indicado.

Python

A ideia é a mesma: use regex para obter o timestamp e o offset (ou use UTC quando este não for encontrado), e em seguida crie uma data. No caso, estou usando o módulo datetime para criar as datas, e o módulo re para expressões regulares:

from datetime import datetime, timezone, timedelta
import re

m = re.match(r'/Date\((\d+)([-+]\d{4})?\)/', '/Date(1555375534143-0300)/')
if m:
    timestamp = int(m.group(1))
    offset = m.group(2)
    if offset:
        total = timedelta(hours = int(offset[1:3]), minutes = int(offset[3:]))
        if offset[0] == '-':
            total = -total
        offset = timezone(total)
    else: # não tem o offset, usar UTC
        offset = timezone.utc

    # o timestamp está em milissegundos, mas fromtimestamp recebe o valor em segundos
    data = datetime.fromtimestamp(timestamp / 1000).astimezone(offset)
    print(data) # 2019-04-15 21:45:34.143000-03:00

PHP

Similar aos demais, basta usar uma expressão regular para obter o timestamp e o offset:

if (preg_match('/Date\((\d+)([-+]\d{4})?\)/', '/Date(1555375534143-0300)/', $matches)) {
    $timestamp = number_format($matches[1] / 1000, 3, '.', '');
    // $matches pode ter até 3 elementos: o match total e os dois grupos de captura
    if (count($matches) === 3) {
        $offset = $matches[2];
    } else {
        $offset = '+0000';
    }
    $data = DateTime::createFromFormat('U.u', $timestamp);
    $data->setTimeZone(new DateTimeZone($offset));
}

Aqui tomamos o cuidado de dividir o timestamp por 1000, pois o formato U.u aceita os segundos, seguido de um ponto, seguido das frações de segundo.

Para saber se o offset está presente, eu vejo se o array de matches tem 3 posições. Isso porque a primeira posição sempre contém todo o trecho que corresponde à expressão, e as posições subsequentes correspondem aos grupos de captura.

JavaScript

Por fim, em JavaScript, a ideia é a mesma: usar uma regex para extrair o timestamp e offset, e criar a data:

let regex = /Date\((\d+)([-+]\d{4})?\)/;
let match = regex.exec('/Date(1555375534143-0300)/').slice(1, 3);
let data = new Date(parseInt(match[0]));

Ao executar a regex, eu pego somente um trecho do array retornado (contendo os dois grupos de captura), já que exec retorna um array com mais informações que não me interessam neste caso.

Um detalhe é que o Date do JavaScript (similar ao java.util.Date do Java) representa um timestamp, e por isso ele não precisa do offset.

Caso queira a data no offset que está na string, uma alternativa é usar a biblioteca Moment.js:

let regex = /Date\((\d+)([-+]\d{4})?\)/;
let match = regex.exec('/Date(1555375534143-0300)/').slice(1, 3);
let offset = match[1] || '+0000';
let data = moment(parseInt(match[0])).utcOffset(offset);
console.log(data.format()); // 2019-04-15T21:45:34-03:00

Outra opção é usar parseZone, que suporta este formato e consegue preservar o offset:

console.log(moment.parseZone("/Date(1555375534143-0300)/").format()); // 2019-04-15T21:45:34-03:00

Resumindo, o formato /Date(1555375534143-0300)/ pode assustar à primeira vista, mas uma vez que entendemos a sua - falta de - lógica, não é difícil extrair dele os dados que precisamos.