Repository: hamba/avro
Pull request #71 (2020-12-21 20:16:13)

Unmarshal union values to their logical type instead of their primitive type
This PR allows to unmarshal unions containing the types `int.date`, `int.time-millis`, `long.time-micros`, `long.timestamp-millis` and `long.timestamp-micros` into a `map[string]interface{}`. Currently when unmarshalling data into 
 `map[string]interface{}`, unions that contain those types are unmarshalled to their primitive type instead of their logical type.

```go
package main

import (
	"fmt"
	"log"
	"time"

	"github.com/hamba/avro"
)

type TimeRecord struct {
	Time *time.Time `avro:"time"`
}

func main() {
	b := []byte{0x02, 0x80, 0xCD, 0xB7, 0xA2, 0xEE, 0xC7, 0xCD, 0x05}

	schema, err := avro.Parse(`{
		"type": "record",
		"name": "TimeRecord",
		"fields": [
			{
				"name": "time",
				"type": ["null", {"type": "long", "logicalType": "timestamp-micros"}]
			}
		]
	}`)

	if err != nil {
		log.Fatal(err)
	}

	var record TimeRecord
	if err = avro.Unmarshal(schema, b, &record); err != nil {
		log.Fatal(err)
	}
	fmt.Println("Decoding into TimeRecord")
	fmt.Printf("Output: %#v\n", record)
	fmt.Printf("Time: %v\n", record.Time)
	fmt.Println()

	var m map[string]interface{}
	if err = avro.Unmarshal(schema, b, &m); err != nil {
		log.Fatal(err)
	}
	fmt.Println("Decoding into map[string]interface{}")
	fmt.Printf("Output: %#v\n", m)
	fmt.Printf("Time: %v\n", m["time"])
	fmt.Println()

	var i interface{}
	if err = avro.Unmarshal(schema, b, &i); err != nil {
		log.Fatal(err)
	}
	fmt.Println("Decoding into interface{}")
	fmt.Printf("Output: %#v\n", i)
	fmt.Printf("Time: %v\n", i.(map[string]interface{})["time"])
	fmt.Println()
}
```

```bash
Decoding into TimeRecord
Output: main.TimeRecord{Time:(*time.Time)(0xc00000c980)}
Time: 2020-01-02 03:04:05 +0000 UTC

Decoding into map[string]interface{}
Output: map[string]interface {}{"time":1577934245000000}
Time: 1577934245000000

Decoding into interface{}
Output: map[string]interface {}{"time":map[string]interface {}{"long":time.Time{wall:0x0, ext:63713531045, loc:(*time.Location)(nil)}}}
Time: map[long:2020-01-02 03:04:05 +0000 UTC]
```



Patch:
diff --git a/resolver.go b/resolver.go
index f3e4983..4ff6129 100644
--- a/resolver.go
+++ b/resolver.go
@@ -2,6 +2,8 @@ package avro
 
 import (
 	"fmt"
+	"math/big"
+	"time"
 
 	"github.com/modern-go/concurrent"
 	"github.com/modern-go/reflect2"
@@ -34,6 +36,14 @@ func NewTypeResolver() *TypeResolver {
 	r.Register(string(Bytes), []byte{})
 	r.Register(string(Boolean), bool(true))
 
+	// Register logical types
+	r.Register(string(Int)+"."+string(Date), time.Time{})
+	r.Register(string(Int)+"."+string(TimeMillis), time.Duration(0))
+	r.Register(string(Long)+"."+string(TimestampMillis), time.Time{})
+	r.Register(string(Long)+"."+string(TimestampMicros), time.Time{})
+	r.Register(string(Long)+"."+string(TimeMicros), time.Duration(0))
+	r.Register(string(Bytes)+"."+string(Decimal), big.Rat{})
+
 	return r
 }
 
diff --git a/schema.go b/schema.go
index a9165dc..513bbfd 100644
--- a/schema.go
+++ b/schema.go
@@ -1061,5 +1061,10 @@ func schemaTypeName(schema Schema) string {
 		return n.FullName()
 	}
 
-	return string(schema.Type())
+	name := string(schema.Type())
+	if lt := getLogicalType(schema); lt != "" {
+		name += "." + string(lt)
+	}
+
+	return name
 }


Test patch:
diff --git a/decoder_union_test.go b/decoder_union_test.go
index 85ad3d4..60e08fa 100644
--- a/decoder_union_test.go
+++ b/decoder_union_test.go
@@ -2,7 +2,9 @@ package avro_test
 
 import (
 	"bytes"
+	"math/big"
 	"testing"
+	"time"
 
 	"github.com/hamba/avro"
 	"github.com/stretchr/testify/assert"
@@ -406,6 +408,49 @@ func TestDecoder_UnionInterfaceUnresolvableType(t *testing.T) {
 	assert.Equal(t, "foo", m["test"].(map[string]interface{})["b"])
 }
 
+func TestDecoder_UnionInterfaceWithTime(t *testing.T) {
+	defer ConfigTeardown()
+
+	data := []byte{0x02, 0x80, 0xCD, 0xB7, 0xA2, 0xEE, 0xC7, 0xCD, 0x05}
+	schema := `{"type": "record", "name": "test", "fields" : [{"name": "a", "type": ["null", {"type": "long", "logicalType": "timestamp-micros"}]}]}`
+	dec, _ := avro.NewDecoder(schema, bytes.NewReader(data))
+
+	var got map[string]interface{}
+	err := dec.Decode(&got)
+
+	assert.NoError(t, err)
+	assert.Equal(t, time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC), got["a"])
+}
+
+func TestDecoder_UnionInterfaceWithDuration(t *testing.T) {
+	defer ConfigTeardown()
+
+	data := []byte{0x02, 0xAA, 0xB4, 0xDE, 0x75}
+	schema := `{"type": "record", "name": "test", "fields" : [{"name": "a", "type": ["null", {"type": "int", "logicalType": "time-millis"}]}]}`
+	dec, _ := avro.NewDecoder(schema, bytes.NewReader(data))
+
+	var got map[string]interface{}
+	err := dec.Decode(&got)
+
+	assert.NoError(t, err)
+	assert.Equal(t, 123456789*time.Millisecond, got["a"])
+}
+
+func TestDecoder_UnionInterfaceWithDecimal(t *testing.T) {
+	defer ConfigTeardown()
+
+	data := []byte{0x02, 0x6, 0x00, 0x87, 0x78}
+	schema := `{"type": "record", "name": "test", "fields" : [{"name": "a", "type": ["null", {"type": "bytes", "logicalType": "decimal", "precision": 4, "scale": 2}]}]}`
+	dec, _ := avro.NewDecoder(schema, bytes.NewReader(data))
+
+	var got map[string]interface{}
+	err := dec.Decode(&got)
+	expected := big.NewRat(1734, 5)
+
+	assert.NoError(t, err)
+	assert.Equal(t, *expected, got["a"])
+}
+
 func TestDecoder_UnionInterfaceUnresolvableTypeWithError(t *testing.T) {
 	defer ConfigTeardown()