Skip to content

spanner: ToStruct does not decode if target field is a pointer to a named type #12576

@jrowland-ccc

Description

@jrowland-ccc

Client

Spanner

Environment

go version go1.24.4 linux/amd64

Code and Dependencies

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"cloud.google.com/go/spanner"
)

type CustomString string

type MyBigStruct struct {
	ExampleId     string
	ExampleString *CustomString
}

func main() {
	if len(os.Args) < 3 {
		log.Fatal("Usage: spanner-bug project-id instance database")
	}
	project := os.Args[1]
	instance := os.Args[2]
	database := os.Args[3]

	dsn := fmt.Sprintf("projects/%s/instances/%s/databases/%s", project, instance, database)
	ctx := context.Background()

	spannerClient, err := spanner.NewClient(ctx, dsn)
	if err != nil {
		log.Fatalf("Failed to create client %v", err)
	}
	_, err = spannerClient.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
		stmt := spanner.NewStatement(`INSERT ExampleTable (ExampleId, ExampleString) VALUES (@ExampleId, @ExampleString)`)
		stmt.Params["ExampleId"] = "id-123"
		stmt.Params["ExampleString"] = "foobar"
		_, err = txn.Update(ctx, stmt)
		return err
	})
	if err != nil {
		log.Fatalf("Failed to insert data: %v", err)
	}
	stmt := spanner.NewStatement(`SELECT ExampleId, ExampleString FROM ExampleTable where ExampleId = @exampleId LIMIT 1`)
	stmt.Params["ExampleId"] = "id-123"

	iter := spannerClient.Single().Query(ctx, stmt)
	row, err := iter.Next()
	if err != nil {
		log.Fatalf("failed to get example row from iterator: %v", err)
	}
	var e MyBigStruct
	err = row.ToStruct(&e)
	if err != nil {
		log.Fatalf("failed to call ToStruct on example row: %v", err)
	}
}
go.mod module spannerbug

go 1.24.4

require cloud.google.com/go/spanner v1.83.0

require (
cel.dev/expr v0.23.0 // indirect
cloud.google.com/go v0.121.2 // indirect
cloud.google.com/go/auth v0.16.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.7.0 // indirect
cloud.google.com/go/monitoring v1.24.2 // indirect
github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect
github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
github.com/googleapis/gax-go/v2 v2.14.2 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
github.com/zeebo/errs v1.4.0 // indirect
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.41.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
golang.org/x/time v0.12.0 // indirect
google.golang.org/api v0.237.0 // indirect
google.golang.org/genproto v0.0.0-20250505200425-f936aa4a68b2 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/grpc v1.73.0 // indirect
google.golang.org/protobuf v1.36.6 // indirect
)

module modname

go 1.24.0

require (
   // ...
)

Expected behavior

.ToStruct or .ToStructLenient is able to decode a string into a pointer to a custom string type.

Actual behavior

Both .ToStruct and .ToStructLenient return an error when trying to decode a string into a pointer to a custom string type:

spanner: code = "InvalidArgument", desc = "cannot decode field ExampleString of Cloud Spanner STRUCT fields:{name:\"ExampleId\"  type:{code:STRING}}  fields:{name:\"ExampleString\"  type:{code:STRING}}, type **main.CustomString cannot be used for decoding STRING"

Additional context

This looks like it's unhandled but expected behavior based off the comments and logic in spanner/value.go's getDecodableSpannerType() and decodeValueToCustomType().

Metadata

Metadata

Assignees

Labels

api: spannerIssues related to the Spanner API.triage meI really want to be triaged.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions