diff --git a/CHANGELOG.md b/CHANGELOG.md
index bc95e9b..a0e5bcb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -15,6 +15,7 @@ existing `0.1.0` release; earlier development prereleases are not listed.
### Fixed
+- Preserve Identity `tools` arrays and explicit null when loading JSON into request parameters, so empty-array replacement and clearing overrides retain their intended semantics.
- Align Forward Outcome rubric objects and Managed Agent/Dream model configuration with the official API. Dream model defaults to `auto` when omitted; object configuration supports `effort` and `context_window`.
### Breaking changes
diff --git a/docs/api/reference.md b/docs/api/reference.md
index 4944ff0..0f45ae8 100644
--- a/docs/api/reference.md
+++ b/docs/api/reference.md
@@ -399,6 +399,7 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward"
- [func \(r \*IdentityConfigSpec\) UnmarshalJSON\(data \[\]byte\) error](<#IdentityConfigSpec.UnmarshalJSON>)
- [type IdentityConfigSpecParam](<#IdentityConfigSpecParam>)
- [func \(r IdentityConfigSpecParam\) MarshalJSON\(\) \(\[\]byte, error\)](<#IdentityConfigSpecParam.MarshalJSON>)
+ - [func \(r \*IdentityConfigSpecParam\) UnmarshalAPIJSON\(data \[\]byte\) error](<#IdentityConfigSpecParam.UnmarshalAPIJSON>)
- [func \(r \*IdentityConfigSpecParam\) UnmarshalJSON\(data \[\]byte\) error](<#IdentityConfigSpecParam.UnmarshalJSON>)
- [type IdentityConfigUpsertParams](<#IdentityConfigUpsertParams>)
- [func \(r IdentityConfigUpsertParams\) MarshalJSON\(\) \(\[\]byte, error\)](<#IdentityConfigUpsertParams.MarshalJSON>)
@@ -453,6 +454,7 @@ import "github.com/QoderAI/qoder-cloud-agents-sdk-go/forward"
- [func \(r \*IdentityToolsUnion\) UnmarshalJSON\(data \[\]byte\) error](<#IdentityToolsUnion.UnmarshalJSON>)
- [type IdentityToolsUnionParam](<#IdentityToolsUnionParam>)
- [func \(r IdentityToolsUnionParam\) MarshalJSON\(\) \(\[\]byte, error\)](<#IdentityToolsUnionParam.MarshalJSON>)
+ - [func \(r \*IdentityToolsUnionParam\) UnmarshalAPIJSON\(data \[\]byte\) error](<#IdentityToolsUnionParam.UnmarshalAPIJSON>)
- [func \(r \*IdentityToolsUnionParam\) UnmarshalJSON\(data \[\]byte\) error](<#IdentityToolsUnionParam.UnmarshalJSON>)
- [type IdentityUpdateParams](<#IdentityUpdateParams>)
- [func \(r IdentityUpdateParams\) MarshalJSON\(\) \(\[\]byte, error\)](<#IdentityUpdateParams.MarshalJSON>)
@@ -6031,6 +6033,15 @@ func (r IdentityConfigSpecParam) MarshalJSON() ([]byte, error)
+
+### func \(\*IdentityConfigSpecParam\) [UnmarshalAPIJSON]()
+
+```go
+func (r *IdentityConfigSpecParam) UnmarshalAPIJSON(data []byte) error
+```
+
+
+
### func \(\*IdentityConfigSpecParam\) [UnmarshalJSON]()
@@ -6635,6 +6646,15 @@ func (r IdentityToolsUnionParam) MarshalJSON() ([]byte, error)
+
+### func \(\*IdentityToolsUnionParam\) [UnmarshalAPIJSON]()
+
+```go
+func (r *IdentityToolsUnionParam) UnmarshalAPIJSON(data []byte) error
+```
+
+
+
### func \(\*IdentityToolsUnionParam\) [UnmarshalJSON]()
diff --git a/forward/configuration_types.go b/forward/configuration_types.go
index 329802b..8bc246e 100644
--- a/forward/configuration_types.go
+++ b/forward/configuration_types.go
@@ -1,6 +1,7 @@
package forward
import (
+ "bytes"
"encoding/json"
"fmt"
"github.com/QoderAI/qoder-cloud-agents-sdk-go/convention/apijson"
@@ -609,8 +610,22 @@ func (r IdentityConfigSpecParam) MarshalJSON() ([]byte, error) {
type shadow IdentityConfigSpecParam
return param.MarshalObject(r, (*shadow)(&r))
}
+
+func (r *IdentityConfigSpecParam) UnmarshalAPIJSON(data []byte) error { return r.UnmarshalJSON(data) }
+
func (r *IdentityConfigSpecParam) UnmarshalJSON(data []byte) error {
- return apijson.UnmarshalRoot(data, r)
+ if err := apijson.UnmarshalRoot(data, r); err != nil {
+ return err
+ }
+ var fields map[string]json.RawMessage
+ if err := json.Unmarshal(data, &fields); err != nil {
+ return err
+ }
+ if tools, ok := fields["tools"]; ok {
+ // The generic decoder treats null union fields as absent.
+ return r.Tools.UnmarshalJSON(tools)
+ }
+ return nil
}
type SessionEventParam struct {
@@ -671,8 +686,25 @@ type IdentityToolsUnionParam struct {
func (r IdentityToolsUnionParam) MarshalJSON() ([]byte, error) {
return param.MarshalUnion(r, r.OfOverrides, r.OfArray)
}
+
+func (r *IdentityToolsUnionParam) UnmarshalAPIJSON(data []byte) error { return r.UnmarshalJSON(data) }
+
func (r *IdentityToolsUnionParam) UnmarshalJSON(data []byte) error {
- return apijson.UnmarshalRoot(data, r)
+ *r = IdentityToolsUnionParam{}
+ data = bytes.TrimSpace(data)
+ if bytes.Equal(data, []byte("null")) {
+ *r = param.NullStruct[IdentityToolsUnionParam]()
+ return nil
+ }
+ if len(data) > 0 {
+ switch data[0] {
+ case '[':
+ return json.Unmarshal(data, &r.OfArray)
+ case '{':
+ return json.Unmarshal(data, &r.OfOverrides)
+ }
+ }
+ return fmt.Errorf("identity tools must be an object, an array, or null")
}
type IdentityToolsUnion struct {
diff --git a/forward/official_contract_test.go b/forward/official_contract_test.go
index 5bab9df..70a7f9b 100644
--- a/forward/official_contract_test.go
+++ b/forward/official_contract_test.go
@@ -116,6 +116,44 @@ func TestOfficialIdentityNullAndArrayOverrides(t *testing.T) {
}
}
+func TestOfficialIdentityToolsJSONRoundTrip(t *testing.T) {
+ for _, tools := range []string{
+ `[]`,
+ `[{"type":"agent_toolset_20260401"}]`,
+ `{}`,
+ `{"Bash":{"enabled":false}}`,
+ `null`,
+ } {
+ t.Run(tools, func(t *testing.T) {
+ var params forward.IdentityConfigUpsertParams
+ input := `{"identity_config":{"tools":` + tools + `}}`
+ if err := json.Unmarshal([]byte(input), ¶ms); err != nil {
+ t.Fatal(err)
+ }
+ client := testClient(func(req *http.Request) (*http.Response, error) {
+ body, err := io.ReadAll(req.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ var got, want any
+ if err := json.Unmarshal(body, &got); err != nil {
+ t.Fatal(err)
+ }
+ if err := json.Unmarshal([]byte(input), &want); err != nil {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("request changed from %s to %s", input, body)
+ }
+ return reply(req, 200, `{}`), nil
+ })
+ if _, err := client.Identities.Configs.Upsert(context.Background(), "idn", "tmpl", params); err != nil {
+ t.Fatal(err)
+ }
+ })
+ }
+}
+
func TestOfficialMountPaginationRetainsScope(t *testing.T) {
for _, reverse := range []bool{false, true} {
calls := 0