Struct Types Usage

JSON is the most common serialization format for GraphQL servers, and GraphQLClient assumes a JSON response. As explained in Custom Types, GraphQLClient deserialises this response by doing JSON3.read(response, GQLResponse{T}) where T defaults to Any but can be configured by using the output_type positional argument. The main benefit of configuring this is to provide type stability to functions that use the response.

Typically, fields in a response will have different types and therefore a struct rather than a Dict is better for type stability as structs can have fields of different types, where as values of a dictionary are either of the same type or of an abstract type.

To deserialise the response into structs, define a type that matches the expected output and set its StructType. This enables JSON3 to deserialise directly into that type.

Because the GraphQL specification requires that fields of the response are in the same order as fields in the operation, we know the order of fields in the response and can therefore use the StructType of OrderedStruct, which is very efficient (although note this doesn't check the field names, it simply deserialises each field in order). See the JSON3 documentation for more information about the StructTypes that can be used, along with other serialisation and deserialisation tricks.

For example

julia> response_str = "{\"data\":{\"MyQuery\":{\"field1\":1,\"field2\":2}}}";

julia> print(GraphQLClient.prettify_query(response_str))
{
    "data":{
        "MyQuery":{
            "field1":1
            "field2":2
        }
    }
}

julia> struct MyQuery
           field1::Int
           field2::Int
       end

julia> StructTypes.StructType(::Type{MyQuery}) = StructTypes.OrderedStruct()

julia> JSON3.read(response_str, GQLResponse{MyQuery})
GQLResponse{MyQuery}
  data: Dict{String, Union{Nothing, MyQuery}}
          MyQuery: MyQuery

Handling NULL fields

The fields of GraphQL responses are often nullable, i.e. they can be nothing. If this occurred in the above example, then deserialisation would fail as it attempts to read null as an integer.

julia> response_str = "{\"data\":{\"MyQuery\":{\"field1\":1,\"field2\":null}}}";

julia> print(GraphQLClient.prettify_query(response_str))
{
    "data":{
        "MyQuery":{
            "field1":1
            "field2":null
        }
    }
}

julia> JSON3.read(response_str, GQLResponse{MyQuery})
ERROR: ArgumentError: invalid JSON at byte position 63 while parsing type Int64: InvalidChar
  ry":{"field1":1,"field2":null}}}

In cases where this is due to an error from the server, then GraphQLClient will attempt to deserialise with Any to read the errors (see Response - Errors for further details), but when this is not due to an error we ideally want to be able to handle it without throwing an exception.

To do this we can make field types a Union without a significant affect on performance - it is slightly slower, but compared to the cost of performing an HTTP request this difference is usually neglible.

julia> struct MyQueryNullable
           field1::Union{Nothing, Int}
           field2::Union{Nothing, Int}
       end

julia> StructTypes.StructType(::Type{MyQueryNullable}) = StructTypes.OrderedStruct()

julia> JSON3.read(response_str, GQLResponse{MyQueryNullable})
GQLResponse{MyQueryNullable}
  data: Dict{String, Union{Nothing, MyQueryNullable}}
          MyQuery: MyQueryNullable