Go v2 vs v3 SDKs: Unknown Fields and Raw JSON

A key consideration in Go SDK design is how to handle unknown request and response fields and shapes, and relatedly accessing raw JSON data. Here’s how these compare between v2 and v3.

v2 uses separate request and response types. v3 proposes unified types and introduces an APIData datastructure to capture intent outside of spec’d fields.

type ConfigParam struct {
    DarkMode bool           `json:"dark_mode"`
    Tabs     TabConfigParam `json:"tab_config"`
    paramObj
}
type TabConfigParam struct {
    Spaces   int  `json:"spaces"`
    HardTabs bool `json:"hard_tabs"`
    paramObj
}

type Config struct {
    DarkMode bool      `json:"dark_mode"`
    Tabs     TabConfig `json:"tab_config"`
    JSON struct {
        DarkMode    respjson.Field
        Tabs        respjson.Field
        ExtraFields map[string]respjson.Field
        raw         string
    } `json:"-"`
}
type TabConfig struct {
    Spaces   int  `json:"spaces"`
    HardTabs bool `json:"hard_tabs"`
    JSON struct {
        Spaces      respjson.Field
        HardTabs    respjson.Field
        ExtraFields map[string]respjson.Field
        raw         string
    } `json:"-"`
}

configReq := api.ConfigParam{}
type Config struct {
    DarkMode bool                 `json:"dark_mode"`
    Tabs     TabConfig            `json:"tab_config"`
    APIData apidata.APIData `json:"-"`
}
type TabConfig struct {
    Spaces   int                  `json:"spaces"`
    HardTabs bool                 `json:"hard_tabs"`
    APIData apidata.APIData `json:"-"`
}

configReq := api.Config{}

For adding undocumented fields in requests, the WithJSONSet option remains.

client.SetConfig(
    configReq,
    option.WithJSONSet("enable_new_feature", true),
)
client.SetConfig(
    configReq,
    option.WithJSONSet("enable_new_feature", true),
)

The alternative approach, changing configReq, goes from using an any map to a simple key / value.

configReq.SetExtraFields(map[string]any{
    "enable_new_feature": true,
})
client.SetConfig(configReq)
configReq.SetField("enable_new_feature", true)
client.SetConfig(configReq)

Adding nested undocumented fields follows the same patterns.

client.SetConfig(
    configReq,
    option.WithJSONSet("tab_config.prefer_vertical_tabs", true),
)

configReq.Tabs.SetExtraFields(map[string]any{
    "prefer_vertical_tabs": true,
})
client.SetConfig(configReq)
client.SetConfig(
    configReq,
    option.WithJSONSet("tab_config.prefer_vertical_tabs", true),
)

configReq.Tabs.SetField("prefer_vertical_tabs", true)
client.SetConfig(configReq)

Sending undocumented request shapes can still use the WithRequestBody approach.

client.SetConfig(configReq, option.WithRequestBody(123))
client.SetConfig(configReq, option.WithRequestBody(123))

But the param.Override approach is replaced with mutation methods on APIData.

configReq := param.Override[api.ConfigParam](123)
client.SetConfig(configReq)
configReq := api.Config{
    APIData: apidata.ReplaceWholeObject(123),
}
client.SetConfig(configReq)

When sending undocumented value types inside objects, WithJSONSet still works.

client.SetConfig(
    configReq,
    option.WithJSONSet("dark_mode", "auto"),
)
client.SetConfig(
    configReq,
    option.WithJSONSet("dark_mode", "auto"),
)

In v3, SetField also works — it overrides the field with the new type.

configReq.SetField("dark_mode", "auto")
client.SetConfig(configReq)

When accessing undocumented response fields, v2 returns a respjson.Field where v3 returns the raw value directly.

field := response.JSON.ExtraFields["enable_new_feature"]
raw := field.Raw()
raw, ok := response.GetField("enable_new_feature")

Accessing undocumented nested fields is also cleaner in v3.

extra := response.Tabs.JSON.ExtraFields
raw := extra["prefer_vertical_tabs"].Raw()
raw, ok := response.Tabs.GetField("prefer_vertical_tabs")

Accessing raw response values for the whole body and fields is unchanged.

var raw []byte
_, err = client.GetConfig(
    ..., option.WithResponseBodyInto(&raw),
)

config, err := client.GetConfig(...)
raw := config.RawJSON()

innerRaw := config.Tabs.RawJSON()
var raw []byte
_, err = client.GetConfig(
    ..., option.WithResponseBodyInto(&raw),
)

config, err := client.GetConfig(...)
raw := config.RawJSON()

innerRaw := config.Tabs.RawJSON()

Next example: Nullable and Optional Fields.