Robert's Data Science Blog

Struct Mapping with JSON3

The JSON3 package for Julia offers to parse JSON directly into structs. In this post I demonstrate this with a JWT response.

I receive a response with a body like the following

{
    "access_token": "<access token>",
      "token_type": "bearer",
      "expires_in": 1199,
   "refresh_token": "<refresh token>",
         ".issued": "2021-12-04T07:42:15.6293508Z",
        ".expires": "2021-12-05T07:42:15.6293508Z"
}

Note that the resolution of the timestamps is finer than milliseconds. The standard library Dates package handles milliseconds, but not micro- or nanoseconds. For starters, I use the TimesDates package to circumvent this.

I would like to map this to a struct with similar fields:

struct JSONWebToken
    AccessToken::String
    TokenType::String
    ExpiresIn::Int64
    RefreshToken::String
    Issued::TimesDates.TimeDateZone
    Expires::TimesDates.TimeDateZone
end

The StructTypes package enables the desired parsing. First we declare that JSONWebToken should be used for parsing:

StructTypes.StructType(::Type{JSONWebToken}) = StructTypes.Struct()

Then we specify a mapping between fields in the JSON string and the struct:

StructTypes.names(::Type{JSONWebToken}) = (
    (:AccessToken, :access_token),
    (:TokenType, :token_type),
    (:ExpiresIn, :expires_in),
    (:RefreshToken, :refresh_token),
    (:Issued, Symbol(".issued")),
    (:Expires, Symbol(".expires"))
)

Many popular types (including DateTime) have converters from StructTypes, but TimesDates.TimeDateZone does not. This is parsed from a string, so we specify it as such:

StructTypes.StructType(::Type{TimesDates.TimeDateZone}) = StructTypes.StringType()

Now we can perform the parsing (here imagining a response from the HTTP package):

JSON3.read(response.body, JSONWebToken)

What if we for some reason want the Issued/Expires field in JSONWebToken to be a Dates.DateTime instead? Simply changing the type of field does not work because the Dates package refuse to parse a string with (a variable number of digits in) sub-milliseconds precision.

We can get around this by truncating the precision in the string and redefining how a string is parsed to a Dates.DateTime (borrowing from the StructTypes package):

function StructTypes.construct(::Type{Dates.DateTime}, x::String; kw...)
    dt, ms = split(t, '.')
    Dates.DateTime(dt * "." * ms[1:3]; kw...)
end