Skip to content

chore: JSON attributes in auto-generated resources #3445

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 20 commits into from
Jul 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ require (

require (
github.com/hashicorp/terraform-json v0.25.0
github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0
go.mongodb.org/atlas-sdk/v20250312004 v20250312004.0.0
)

Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,8 @@ github.com/hashicorp/terraform-json v0.25.0 h1:rmNqc/CIfcWawGiwXmRuiXJKEiJu1ntGo
github.com/hashicorp/terraform-json v0.25.0/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc=
github.com/hashicorp/terraform-plugin-framework v1.15.0 h1:LQ2rsOfmDLxcn5EeIwdXFtr03FVsNktbbBci8cOKdb4=
github.com/hashicorp/terraform-plugin-framework v1.15.0/go.mod h1:hxrNI/GY32KPISpWqlCoTLM9JZsGH3CyYlir09bD/fI=
github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0 h1:SJXL5FfJJm17554Kpt9jFXngdM6fXbnUnZ6iT2IeiYA=
github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0/go.mod h1:p0phD0IYhsu9bR4+6OetVvvH59I6LwjXGnTVEr8ox6E=
github.com/hashicorp/terraform-plugin-framework-timeouts v0.5.0 h1:I/N0g/eLZ1ZkLZXUQ0oRSXa8YG/EF0CEuQP1wXdrzKw=
github.com/hashicorp/terraform-plugin-framework-timeouts v0.5.0/go.mod h1:t339KhmxnaF4SzdpxmqW8HnQBHVGYazwtfxU0qCs4eE=
github.com/hashicorp/terraform-plugin-framework-validators v0.18.0 h1:OQnlOt98ua//rCw+QhBbSqfW3QbwtVrcdWeQN5gI3Hw=
Expand Down
9 changes: 8 additions & 1 deletion internal/common/autogen/marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"reflect"

"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
)
Expand Down Expand Up @@ -105,8 +106,14 @@ func getModelAttr(val attr.Value) (any, error) {
return getListAttr(v.Elements())
case types.Set:
return getListAttr(v.Elements())
case jsontypes.Normalized:
var valueJSON any
if err := json.Unmarshal([]byte(v.ValueString()), &valueJSON); err != nil {
return nil, fmt.Errorf("marshal failed for JSON custom type: %v", err)
}
return valueJSON, nil
default:
return nil, fmt.Errorf("unmarshal not supported yet for type %T", v)
return nil, fmt.Errorf("marshal not supported yet for type %T", v)
}
}

Expand Down
26 changes: 14 additions & 12 deletions internal/common/autogen/marshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package autogen_test
import (
"testing"

"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/autogen"
Expand Down Expand Up @@ -42,17 +43,17 @@ const epsilon = 10e-15 // float tolerance in test equality

func TestMarshalBasic(t *testing.T) {
model := struct {
AttrFloat types.Float64 `tfsdk:"attr_float"`
AttrString types.String `tfsdk:"attr_string"`
// values with tag `omitjson` are not marshaled, and they don't need to be Terraform types
AttrOmit types.String `tfsdk:"attr_omit" autogen:"omitjson"`
AttrOmitNoTerraform string `autogen:"omitjson"`
AttrUnkown types.String `tfsdk:"attr_unknown"`
AttrNull types.String `tfsdk:"attr_null"`
AttrInt types.Int64 `tfsdk:"attr_int"`
AttrBoolTrue types.Bool `tfsdk:"attr_bool_true"`
AttrBoolFalse types.Bool `tfsdk:"attr_bool_false"`
AttrBoolNull types.Bool `tfsdk:"attr_bool_null"`
AttrFloat types.Float64 `tfsdk:"attr_float"`
AttrString types.String `tfsdk:"attr_string"`
AttrOmit types.String `tfsdk:"attr_omit" autogen:"omitjson"`
AttrUnkown types.String `tfsdk:"attr_unknown"`
AttrNull types.String `tfsdk:"attr_null"`
AttrJSON jsontypes.Normalized `tfsdk:"attr_json"`
AttrOmitNoTerraform string `autogen:"omitjson"`
AttrInt types.Int64 `tfsdk:"attr_int"`
AttrBoolTrue types.Bool `tfsdk:"attr_bool_true"`
AttrBoolFalse types.Bool `tfsdk:"attr_bool_false"`
AttrBoolNull types.Bool `tfsdk:"attr_bool_null"`
}{
AttrFloat: types.Float64Value(1.234),
AttrString: types.StringValue("hello"),
Expand All @@ -64,8 +65,9 @@ func TestMarshalBasic(t *testing.T) {
AttrBoolTrue: types.BoolValue(true),
AttrBoolFalse: types.BoolValue(false),
AttrBoolNull: types.BoolNull(), // null values are not marshaled
AttrJSON: jsontypes.NewNormalizedValue("{\"hello\": \"there\"}"),
}
const expectedJSON = `{ "attrString": "hello", "attrInt": 1, "attrFloat": 1.234, "attrBoolTrue": true, "attrBoolFalse": false }`
const expectedJSON = `{ "attrString": "hello", "attrInt": 1, "attrFloat": 1.234, "attrBoolTrue": true, "attrBoolFalse": false, "attrJSON": {"hello": "there"} }`
raw, err := autogen.Marshal(&model, false)
require.NoError(t, err)
assert.JSONEq(t, expectedJSON, string(raw))
Expand Down
4 changes: 4 additions & 0 deletions internal/common/autogen/unknown.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"reflect"

"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
)
Expand Down Expand Up @@ -116,6 +117,9 @@ func getNullAttr(attrType attr.Type) (attr.Value, error) {
if mapType, ok := attrType.(types.MapType); ok {
return types.MapNull(mapType.ElemType), nil
}
if _, ok := attrType.(jsontypes.NormalizedType); ok {
return jsontypes.NewNormalizedNull(), nil
}
return nil, fmt.Errorf("unmarshal to get null value not supported yet for type %T", attrType)
}
}
15 changes: 15 additions & 0 deletions internal/common/autogen/unmarshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"reflect"
"strings"

"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
)
Expand Down Expand Up @@ -108,6 +109,13 @@ func getTfAttr(value any, valueType attr.Type, oldVal attr.Value, name string) (
}
return nil, errUnmarshal(value, valueType, "Number", nameErr)
case map[string]any:
if _, ok := valueType.(jsontypes.NormalizedType); ok {
jsonBytes, err := json.Marshal(v)
if err != nil {
return nil, fmt.Errorf("failed to marshal object to JSON for attribute %s: %v", nameErr, err)
}
return jsontypes.NewNormalizedValue(string(jsonBytes)), nil
}
if obj, ok := oldVal.(types.Object); ok {
objNew, err := setObjAttrModel(obj, v)
if err != nil {
Expand All @@ -124,6 +132,13 @@ func getTfAttr(value any, valueType attr.Type, oldVal attr.Value, name string) (
}
return nil, errUnmarshal(value, valueType, "Object", nameErr)
case []any:
if _, ok := valueType.(jsontypes.NormalizedType); ok {
jsonBytes, err := json.Marshal(v)
if err != nil {
return nil, fmt.Errorf("failed to marshal array to JSON for attribute %s: %v", nameErr, err)
}
return jsontypes.NewNormalizedValue(string(jsonBytes)), nil
}
if list, ok := oldVal.(types.List); ok {
listNew, err := setListAttrModel(list, v, nameErr)
if err != nil {
Expand Down
36 changes: 25 additions & 11 deletions internal/common/autogen/unmarshal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package autogen_test
import (
"testing"

"github.com/hashicorp/terraform-plugin-framework-jsontypes/jsontypes"
"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/mongodb/terraform-provider-mongodbatlas/internal/common/autogen"
Expand All @@ -12,14 +13,15 @@ import (

func TestUnmarshalBasic(t *testing.T) {
var model struct {
AttrFloat types.Float64 `tfsdk:"attr_float"`
AttrFloatWithInt types.Float64 `tfsdk:"attr_float_with_int"`
AttrString types.String `tfsdk:"attr_string"`
AttrNotInJSON types.String `tfsdk:"attr_not_in_json"`
AttrInt types.Int64 `tfsdk:"attr_int"`
AttrIntWithFloat types.Int64 `tfsdk:"attr_int_with_float"`
AttrTrue types.Bool `tfsdk:"attr_true"`
AttrFalse types.Bool `tfsdk:"attr_false"`
AttrFloat types.Float64 `tfsdk:"attr_float"`
AttrFloatWithInt types.Float64 `tfsdk:"attr_float_with_int"`
AttrString types.String `tfsdk:"attr_string"`
AttrNotInJSON types.String `tfsdk:"attr_not_in_json"`
AttrJSON jsontypes.Normalized `tfsdk:"attr_json"`
AttrInt types.Int64 `tfsdk:"attr_int"`
AttrIntWithFloat types.Int64 `tfsdk:"attr_int_with_float"`
AttrTrue types.Bool `tfsdk:"attr_true"`
AttrFalse types.Bool `tfsdk:"attr_false"`
}
const (
// attribute_not_in_model is ignored because it is not in the model, no error is thrown.
Expand All @@ -34,7 +36,8 @@ func TestUnmarshalBasic(t *testing.T) {
"attrFloat": 456.1,
"attrFloatWithInt": 13,
"attrNotInModel": "val",
"attrNull": null
"attrNull": null,
"attrJSON": {"hello": "there"}
}
`
)
Expand All @@ -47,6 +50,7 @@ func TestUnmarshalBasic(t *testing.T) {
assert.InEpsilon(t, float64(456.1), model.AttrFloat.ValueFloat64(), epsilon)
assert.InEpsilon(t, float64(13), model.AttrFloatWithInt.ValueFloat64(), epsilon)
assert.True(t, model.AttrNotInJSON.IsNull()) // attributes not in JSON response are not changed, so null is kept.
assert.JSONEq(t, "{\"hello\":\"there\"}", model.AttrJSON.ValueString())
}

func TestUnmarshalNestedAllTypes(t *testing.T) {
Expand All @@ -67,6 +71,7 @@ func TestUnmarshalNestedAllTypes(t *testing.T) {
AttrMapSimple types.Map `tfsdk:"attr_map_simple"`
AttrMapSimpleExisting types.Map `tfsdk:"attr_map_simple_existing"`
AttrMapObj types.Map `tfsdk:"attr_map_obj"`
AttrJSONList types.List `tfsdk:"attr_json_list"`
}
model := modelst{
AttrObj: types.ObjectValueMust(objTypeTest.AttrTypes, map[string]attr.Value{
Expand Down Expand Up @@ -100,7 +105,8 @@ func TestUnmarshalNestedAllTypes(t *testing.T) {
"existing": types.StringValue("valexisting"),
"existingCHANGE": types.StringValue("before"),
}),
AttrMapObj: types.MapUnknown(objTypeTest),
AttrMapObj: types.MapUnknown(objTypeTest),
AttrJSONList: types.ListUnknown(jsontypes.NormalizedType{}),
}
// attrUnexisting is ignored because it is in JSON but not in the model, no error is returned
const (
Expand Down Expand Up @@ -226,7 +232,11 @@ func TestUnmarshalNestedAllTypes(t *testing.T) {
"attrFloat": 22.2,
"attrBool": true
}
}
},
"attrJSONList": [
{"hello1": "there1"},
{"hello2": "there2"}
]
}
`
)
Expand Down Expand Up @@ -375,6 +385,10 @@ func TestUnmarshalNestedAllTypes(t *testing.T) {
"attr_bool": types.BoolValue(true),
}),
}),
AttrJSONList: types.ListValueMust(jsontypes.NormalizedType{}, []attr.Value{
jsontypes.NewNormalizedValue(`{"hello1":"there1"}`),
jsontypes.NewNormalizedValue(`{"hello2":"there2"}`),
}),
}
require.NoError(t, autogen.Unmarshal([]byte(jsonResp), &model))
assert.Equal(t, modelExpected, model)
Expand Down
4 changes: 2 additions & 2 deletions internal/serviceapi/streaminstanceapi/resource_schema.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 14 additions & 0 deletions internal/serviceapi/streamprocessorapi/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package streamprocessorapi_test

import (
"os"
"testing"

"github.com/mongodb/terraform-provider-mongodbatlas/internal/testutil/acc"
)

func TestMain(m *testing.M) {
acc.SetupSharedResources()
exitCode := m.Run()
os.Exit(exitCode)
}
Loading