Skip to content
Open
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
10 changes: 10 additions & 0 deletions .changelog/93dbc158-0acc-4482-b5cb-727186ec548b.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"id": "93dbc158-0acc-4482-b5cb-727186ec548b",
"type": "bugfix",
"description": "attributevalue marshal to AttributeValueMemberNULL with double pointer when outer pointer is not nil",
"collapse": false,
"modules": [
"feature/dynamodb/attributevalue",
"feature/dynamodbstreams/attributevalue"
]
}
92 changes: 92 additions & 0 deletions feature/dynamodb/attributevalue/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1344,3 +1344,95 @@ func TestUnmarshalIndividualSetValues(t *testing.T) {
t.Errorf("expect value match\n%s", diff)
}
}

func TestDecodeDoublePointer(t *testing.T) {
type output struct {
Foo **int `dynamodbav:"Foo"`
}

number := 5
foo := &number

cases := []struct {
input types.AttributeValue
expected output
expectError bool
}{
{
input: &types.AttributeValueMemberM{
Value: nil,
},
expected: output{},
},
{
input: &types.AttributeValueMemberM{
Value: map[string]types.AttributeValue{},
},
expected: output{},
},
{
input: &types.AttributeValueMemberM{
Value: map[string]types.AttributeValue{
"Foo": nil,
},
},
expected: output{},
},
{
input: &types.AttributeValueMemberM{
Value: map[string]types.AttributeValue{
"Foo": &types.AttributeValueMemberNULL{
Value: true,
},
},
},
expected: output{},
},
{
input: &types.AttributeValueMemberM{
Value: map[string]types.AttributeValue{
"Foo": &types.AttributeValueMemberN{
Value: "5",
},
},
},
expected: output{
Foo: &foo,
},
},
{
input: &types.AttributeValueMemberM{
Value: map[string]types.AttributeValue{
"Foo": &types.AttributeValueMemberS{
Value: "5",
},
},
},
expectError: true,
},
}

for i, c := range cases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
var actual output
err := Unmarshal(c.input, &actual)
if c.expectError && err == nil {
t.Error("expected error")
t.Fail()
}
if !c.expectError && err != nil {
t.Errorf("unexpected error: %v", err)
t.Fail()
}

if c.expectError && err != nil {
t.Logf("found expected error: %v", err)
return
}

if diff := cmpDiff(c.expected, actual); diff != "" {
t.Errorf("unexpected diff: %v", diff)
}
})
}
}
30 changes: 25 additions & 5 deletions feature/dynamodb/attributevalue/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,18 @@ func (e *Encoder) encode(v reflect.Value, fieldTag tag) (types.AttributeValue, e
case reflect.Chan, reflect.Func, reflect.UnsafePointer:
// skip unsupported types
return nil, nil
// handle double pointers
case reflect.Pointer:
el := v.Elem()
if el.IsValid() {
return e.encode(el, fieldTag)
}

if fieldTag.OmitEmpty && fieldTag.OmitEmptyElem {
return nil, nil
}

return encodeNull(), nil

default:
return e.encodeScalar(v, fieldTag)
Expand Down Expand Up @@ -549,9 +561,19 @@ func (e *Encoder) encodeMap(v reflect.Value, fieldTag tag) (types.AttributeValue
}

elemVal := v.MapIndex(key)
// Both OmitEmpty and OmitEmptyElem are set to fieldTag.OmitEmptyElem here.
// This ensures that omitempty logic is applied both to the map element itself (OmitEmpty)
// and recursively to any nested elements (OmitEmptyElem), so that omitempty is respected
// at all levels of nested maps or slices. This is necessary for correct DynamoDB marshaling
// when omitemptyelem is specified on the parent map or slice field.
//
// Note: For pointer types (including double pointers), omitempty only omits the field if the outer pointer is nil.
// If the outer pointer is non-nil but the inner pointer is nil (e.g., **int where *int is nil),
// omitempty does not omit the field; instead, the encoder emits a DynamoDB NULL attribute for that field.
elem, err := e.encode(elemVal, tag{
OmitEmpty: fieldTag.OmitEmptyElem,
NullEmpty: fieldTag.NullEmptyElem,
OmitEmpty: fieldTag.OmitEmptyElem,
OmitEmptyElem: fieldTag.OmitEmptyElem,
NullEmpty: fieldTag.NullEmptyElem,
})
if err != nil {
return nil, err
Expand Down Expand Up @@ -807,9 +829,7 @@ func encoderFieldByIndex(v reflect.Value, index []int) (reflect.Value, bool) {
func valueElem(v reflect.Value) reflect.Value {
switch v.Kind() {
case reflect.Interface, reflect.Ptr:
for v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr {
v = v.Elem()
}
v = v.Elem()
}

return v
Expand Down
142 changes: 142 additions & 0 deletions feature/dynamodb/attributevalue/encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -866,3 +866,145 @@ func TestEncodeEmptyTime(t *testing.T) {
t.Errorf("expect %v, got %v", e, a)
}
}

func TestEncodeDoublePointerOmitEmpty(t *testing.T) {
type input struct {
Foo **int `dynamodbav:"Foo,omitempty"`
}
var nilDouble **int
var nilSingle *int
number := 42
nonNil := &number

cases := []struct {
input input
expected types.AttributeValue
}{
{
input: input{
Foo: nil,
},
expected: &types.AttributeValueMemberM{
Value: map[string]types.AttributeValue{},
},
},
{
input: input{
Foo: nilDouble,
},
expected: &types.AttributeValueMemberM{
Value: map[string]types.AttributeValue{},
},
},
{
input: input{
Foo: &nilSingle,
},
expected: &types.AttributeValueMemberM{
Value: map[string]types.AttributeValue{
"Foo": &types.AttributeValueMemberNULL{
Value: true,
},
},
},
},
{
input: input{
Foo: &nonNil,
},
expected: &types.AttributeValueMemberM{
Value: map[string]types.AttributeValue{
"Foo": &types.AttributeValueMemberN{
Value: "42",
},
},
},
},
}

for i, c := range cases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
actual, err := Marshal(c.input)
if err != nil {
t.Errorf("unexpected error: %v", err)
t.Fail()
}

if diff := cmpDiff(c.expected, actual); diff != "" {
t.Errorf("unexpected diff: %v", diff)
}
})
}
}

func TestEncodeDoublePointer(t *testing.T) {
type input struct {
Foo **int `dynamodbav:"Foo"`
}
var nilDouble **int
var nilSingle *int
number := 42
nonNil := &number

cases := []struct {
input input
expected types.AttributeValue
}{
{
input: input{
Foo: nil,
},
expected: &types.AttributeValueMemberM{
Value: map[string]types.AttributeValue{
"Foo": &types.AttributeValueMemberNULL{Value: true},
},
},
},
{
input: input{
Foo: nilDouble,
},
expected: &types.AttributeValueMemberM{
Value: map[string]types.AttributeValue{
"Foo": &types.AttributeValueMemberNULL{Value: true},
},
},
},
{
input: input{
Foo: &nilSingle,
},
expected: &types.AttributeValueMemberM{
Value: map[string]types.AttributeValue{
"Foo": &types.AttributeValueMemberNULL{Value: true},
},
},
},
{
input: input{
Foo: &nonNil,
},
expected: &types.AttributeValueMemberM{
Value: map[string]types.AttributeValue{
"Foo": &types.AttributeValueMemberN{
Value: "42",
},
},
},
},
}

for i, c := range cases {
t.Run(strconv.Itoa(i), func(t *testing.T) {
actual, err := Marshal(c.input)
if err != nil {
t.Errorf("unexpected error: %v", err)
t.Fail()
}

if diff := cmpDiff(c.expected, actual); diff != "" {
t.Errorf("unexpected diff: %v", diff)
}
})
}
}
Loading