fix(datastore): handle opaque type conversions (#14477)

fixes #5225
diff --git a/datastore/load.go b/datastore/load.go
index 7793496..78b5d28 100644
--- a/datastore/load.go
+++ b/datastore/load.go
@@ -287,6 +287,10 @@
 	case reflect.String:
 		x, ok := pValue.(string)
 		if !ok && pValue != nil {
+			if b, ok := pValue.([]byte); ok {
+				v.SetString(string(b))
+				return ""
+			}
 			return typeMismatchReason(p, v)
 		}
 		v.SetString(x)
@@ -348,20 +352,56 @@
 				return err.Error()
 			}
 		case int64:
+			if v.Elem().Type() == typeOfTime {
+				s := x / 1e6
+				ns := (x % 1e6) * 1e3
+				v.Elem().Set(reflect.ValueOf(time.Unix(s, ns).In(time.UTC)))
+				return ""
+			}
+			if v.Elem().Kind() < reflect.Int || v.Elem().Kind() > reflect.Int64 {
+				return typeMismatchReason(p, v)
+			}
 			if v.Elem().OverflowInt(x) {
 				return overflowReason(x, v.Elem())
 			}
 			v.Elem().SetInt(x)
 		case float64:
+			if v.Elem().Kind() != reflect.Float32 && v.Elem().Kind() != reflect.Float64 {
+				return typeMismatchReason(p, v)
+			}
 			if v.Elem().OverflowFloat(x) {
 				return overflowReason(x, v.Elem())
 			}
 			v.Elem().SetFloat(x)
 		case bool:
+			if v.Elem().Kind() != reflect.Bool {
+				return typeMismatchReason(p, v)
+			}
 			v.Elem().SetBool(x)
 		case string:
-			v.Elem().SetString(x)
+			if v.Elem().Kind() == reflect.String {
+				v.Elem().SetString(x)
+				return ""
+			}
+			if v.Elem().Kind() == reflect.Slice && v.Elem().Type().Elem().Kind() == reflect.Uint8 {
+				v.Elem().SetBytes([]byte(x))
+				return ""
+			}
+			return typeMismatchReason(p, v)
+		case []byte:
+			if v.Elem().Kind() == reflect.String {
+				v.Elem().SetString(string(x))
+				return ""
+			}
+			if v.Elem().Kind() == reflect.Slice && v.Elem().Type().Elem().Kind() == reflect.Uint8 {
+				v.Elem().SetBytes(x)
+				return ""
+			}
+			return typeMismatchReason(p, v)
 		case GeoPoint, time.Time:
+			if v.Elem().Type() != reflect.TypeOf(x) {
+				return typeMismatchReason(p, v)
+			}
 			v.Elem().Set(reflect.ValueOf(x))
 		default:
 			return typeMismatchReason(p, v)
@@ -377,7 +417,7 @@
 			micros, ok := pValue.(int64)
 			if ok {
 				s := micros / 1e6
-				ns := micros % 1e6
+				ns := (micros % 1e6) * 1e3
 				v.Set(reflect.ValueOf(time.Unix(s, ns).In(time.UTC)))
 				break
 			}
@@ -412,11 +452,15 @@
 			}
 		}
 	case reflect.Slice:
-		x, ok := pValue.([]byte)
-		if !ok && pValue != nil {
+		if v.Type().Elem().Kind() != reflect.Uint8 {
 			return typeMismatchReason(p, v)
 		}
-		if v.Type().Elem().Kind() != reflect.Uint8 {
+		x, ok := pValue.([]byte)
+		if !ok && pValue != nil {
+			if s, ok := pValue.(string); ok {
+				v.SetBytes([]byte(s))
+				return ""
+			}
 			return typeMismatchReason(p, v)
 		}
 		v.SetBytes(x)
diff --git a/datastore/load_test.go b/datastore/load_test.go
index 381871e..e2e12c5 100644
--- a/datastore/load_test.go
+++ b/datastore/load_test.go
@@ -1389,3 +1389,170 @@
 		}
 	}
 }
+
+func TestIssue5225(t *testing.T) {
+	type CustomType struct {
+		ID   []byte `datastore:"custom_identifier"`
+		Name string `datastore:"name"`
+	}
+
+	// Case 1: Struct has []byte, but Property has string (e.g. from projection)
+	t.Run("String To []byte", func(t *testing.T) {
+		x := &CustomType{}
+		ps := []Property{
+			{Name: "custom_identifier", Value: "some-id"},
+		}
+		err := LoadStruct(x, ps)
+		if err != nil {
+			t.Errorf("LoadStruct failed: %v", err)
+		}
+		if string(x.ID) != "some-id" {
+			t.Errorf("got x.ID = %q, want %q", string(x.ID), "some-id")
+		}
+	})
+
+	// Case 2: Struct has string, but Property has []byte (e.g. from projection)
+	t.Run("[]byte To String", func(t *testing.T) {
+		x := &CustomType{}
+		ps := []Property{
+			{Name: "name", Value: []byte("some-name")},
+		}
+		err := LoadStruct(x, ps)
+		if err != nil {
+			t.Errorf("LoadStruct failed: %v", err)
+		}
+		if x.Name != "some-name" {
+			t.Errorf("got x.Name = %q, want %q", x.Name, "some-name")
+		}
+	})
+
+	// Case 3: Pointer to string, but Property has []byte
+	t.Run("[]byte To *String", func(t *testing.T) {
+		type PtrType struct {
+			Name *string `datastore:"name"`
+		}
+		x := &PtrType{}
+		ps := []Property{
+			{Name: "name", Value: []byte("some-name")},
+		}
+		err := LoadStruct(x, ps)
+		if err != nil {
+			t.Errorf("LoadStruct failed: %v", err)
+		}
+		if x.Name == nil || *x.Name != "some-name" {
+			got := "nil"
+			if x.Name != nil {
+				got = *x.Name
+			}
+			t.Errorf("got x.Name = %q, want %q", got, "some-name")
+		}
+	})
+
+	// Case 4: time.Time, but Property has int64 (micros)
+	t.Run("int64 To time.Time", func(t *testing.T) {
+		type TimeType struct {
+			T time.Time `datastore:"t"`
+		}
+		x := &TimeType{}
+		// 1639728000123456 is 2021-12-17 08:00:00.123456 UTC
+		ps := []Property{
+			{Name: "t", Value: int64(1639728000123456)},
+		}
+		err := LoadStruct(x, ps)
+		if err != nil {
+			t.Errorf("LoadStruct failed: %v", err)
+		}
+		want := time.Unix(1639728000, 123456000).In(time.UTC)
+		if !x.T.Equal(want) {
+			t.Errorf("got x.T = %v, want %v", x.T, want)
+		}
+	})
+
+	// Case 5: *time.Time, but Property has int64 (micros)
+	t.Run("int64 To *time.Time", func(t *testing.T) {
+		type TimePtrType struct {
+			T *time.Time `datastore:"t"`
+		}
+		x := &TimePtrType{}
+		ps := []Property{
+			{Name: "t", Value: int64(1639728000123456)},
+		}
+		err := LoadStruct(x, ps)
+		if err != nil {
+			t.Errorf("LoadStruct failed: %v", err)
+		}
+		want := time.Unix(1639728000, 123456000).In(time.UTC)
+		if x.T == nil || !x.T.Equal(want) {
+			t.Errorf("got x.T = %v, want %v", x.T, want)
+		}
+	})
+
+	// Case 6: *[]byte, but Property has string
+	t.Run("String To *[]byte", func(t *testing.T) {
+		type PtrSliceType struct {
+			ID *[]byte `datastore:"id"`
+		}
+		x := &PtrSliceType{}
+		ps := []Property{
+			{Name: "id", Value: "some-id"},
+		}
+		err := LoadStruct(x, ps)
+		if err != nil {
+			t.Errorf("LoadStruct failed: %v", err)
+		}
+		if x.ID == nil || string(*x.ID) != "some-id" {
+			got := "nil"
+			if x.ID != nil {
+				got = string(*x.ID)
+			}
+			t.Errorf("got x.ID = %q, want %q", got, "some-id")
+		}
+	})
+
+	// Case 7: *[]byte, but Property has []byte
+	t.Run("[]byte To *[]byte", func(t *testing.T) {
+		type PtrSliceType struct {
+			ID *[]byte `datastore:"id"`
+		}
+		x := &PtrSliceType{}
+		ps := []Property{
+			{Name: "id", Value: []byte("some-id")},
+		}
+		err := LoadStruct(x, ps)
+		if err != nil {
+			t.Errorf("LoadStruct failed: %v", err)
+		}
+		if x.ID == nil || string(*x.ID) != "some-id" {
+			got := "nil"
+			if x.ID != nil {
+				got = string(*x.ID)
+			}
+			t.Errorf("got x.ID = %q, want %q", got, "some-id")
+		}
+	})
+
+	// Case 8: Saving *[]byte
+	t.Run("Save *[]byte", func(t *testing.T) {
+		type PtrSliceType struct {
+			ID *[]byte `datastore:"id"`
+		}
+		data := []byte("some-id")
+		x := &PtrSliceType{ID: &data}
+		props, err := SaveStruct(x)
+		if err != nil {
+			t.Errorf("SaveStruct failed: %v", err)
+		}
+		found := false
+		for _, p := range props {
+			if p.Name == "id" {
+				found = true
+				if string(p.Value.([]byte)) != "some-id" {
+					t.Errorf("got p.Value = %q, want %q", string(p.Value.([]byte)), "some-id")
+				}
+			}
+		}
+		if !found {
+			t.Errorf("property 'id' not found in %v", props)
+		}
+	})
+}
diff --git a/datastore/save.go b/datastore/save.go
index 5c1478e..e21fd52 100644
--- a/datastore/save.go
+++ b/datastore/save.go
@@ -520,7 +520,7 @@
 // isValidPointerType reports whether a struct field can be a pointer to type t
 // for the purposes of saving and loading.
 func isValidPointerType(t reflect.Type) bool {
-	if t == typeOfTime || t == typeOfGeoPoint {
+	if t == typeOfTime || t == typeOfGeoPoint || t == typeOfByteSlice {
 		return true
 	}
 	switch t.Kind() {