Repository: hamba/avro
Pull request #292 (2023-08-20 13:08:36)

feat: add encoders to avrogen
This PR adds support for generating encoders and the schema to all generated objects.


Patch:
diff --git a/cmd/avrogen/main.go b/cmd/avrogen/main.go
index 8587ee9..ef28186 100644
--- a/cmd/avrogen/main.go
+++ b/cmd/avrogen/main.go
@@ -19,6 +19,7 @@ type config struct {
 	Out      string
 	Tags     string
 	FullName bool
+	Encoders bool
 }
 
 func main() {
@@ -33,6 +34,7 @@ func realMain(args []string, out io.Writer) int {
 	flgs.StringVar(&cfg.Out, "o", "", "The output file path.")
 	flgs.StringVar(&cfg.Tags, "tags", "", "The additional field tags <tag-name>:{snake|camel|upper-camel|kebab}>[,...]")
 	flgs.BoolVar(&cfg.FullName, "fullname", false, "Use the full name of the Record schema to create the struct name.")
+	flgs.BoolVar(&cfg.Encoders, "encoders", false, "Generate encoders for the structs.")
 	flgs.Usage = func() {
 		_, _ = fmt.Fprintln(out, "Usage: avrogen [options] schemas")
 		_, _ = fmt.Fprintln(out, "Options:")
@@ -52,7 +54,11 @@ func realMain(args []string, out io.Writer) int {
 		return 1
 	}
 
-	g := gen.NewGenerator(cfg.Pkg, tags, gen.WithFullName(cfg.FullName))
+	opts := []gen.OptsFunc{
+		gen.WithFullName(cfg.FullName),
+		gen.WithEncoders(cfg.Encoders),
+	}
+	g := gen.NewGenerator(cfg.Pkg, tags, opts...)
 	for _, file := range flgs.Args() {
 		schema, err := avro.ParseFiles(filepath.Clean(file))
 		if err != nil {
diff --git a/gen/gen.go b/gen/gen.go
index a13c5ee..d942eba 100644
--- a/gen/gen.go
+++ b/gen/gen.go
@@ -19,6 +19,7 @@ type Config struct {
 	PackageName string
 	Tags        map[string]TagStyle
 	FullName    bool
+	Encoders    bool
 }
 
 // TagStyle defines the styling for a tag.
@@ -39,6 +40,7 @@ const outputTemplate = `package {{ .PackageName }}
 
 // Code generated by avro/gen. DO NOT EDIT.
 
+{{- $encoders := .WithEncoders }}
 {{ if len .Imports }}
 import (
 	{{- range .Imports }}
@@ -60,11 +62,36 @@ import (
 {{ end }}
 
 {{- range .Typedefs }}
+// {{ .Name }} is a generated struct.
 type {{ .Name }} struct {
 	{{- range .Fields }}
 		{{ .Name }} {{ .Type }} {{ .Tag }}
 	{{- end }}
+{{- if $encoders }}
+
+    schema avro.Schema
+{{- end }}
+}
+
+{{- if $encoders }}
+// Schema returns the schema for {{ .Name }}.
+func (o *{{ .Name }}) Schema() avro.Schema {
+  if o.schema == nil {
+    o.schema = avro.MustParse(` + "`{{ .Schema }}`" + `)
+  }
+  return o.schema
+}
+
+// Unmarshal decodes b into the receiver.
+func (o *{{ .Name }}) Unmarshal(b []byte) error {
+	return avro.Unmarshal(o.Schema(), b, o)
+}
+
+// Marshal encodes the receiver.
+func (o *{{ .Name }}) Marshal() ([]byte, error) {
+	return avro.Marshal(o.Schema(), o)
 }
+{{- end }}
 {{ end }}`
 
 var primitiveMappings = map[avro.Type]string{
@@ -95,6 +122,7 @@ func StructFromSchema(schema avro.Schema, w io.Writer, cfg Config) error {
 
 	opts := []OptsFunc{
 		WithFullName(cfg.FullName),
+		WithEncoders(cfg.Encoders),
 	}
 	g := NewGenerator(strcase.ToSnake(cfg.PackageName), cfg.Tags, opts...)
 	g.Parse(rec)
@@ -124,11 +152,23 @@ func WithFullName(b bool) OptsFunc {
 	}
 }
 
+// WithEncoders configures the generator to generate schema and encoders on
+// all objects.
+func WithEncoders(b bool) OptsFunc {
+	return func(g *Generator) {
+		g.encoders = b
+		if b {
+			g.thirdPartyImports = append(g.thirdPartyImports, "github.com/hamba/avro/v2")
+		}
+	}
+}
+
 // Generator generates Go structs from schemas.
 type Generator struct {
 	pkg      string
 	tags     map[string]TagStyle
 	fullName bool
+	encoders bool
 
 	imports           []string
 	thirdPartyImports []string
@@ -209,7 +249,7 @@ func (g *Generator) resolveRecordSchema(schema *avro.RecordSchema) string {
 
 	typeName := g.resolveTypeName(schema)
 	if !g.hasTypeDef(typeName) {
-		g.typedefs = append(g.typedefs, newType(typeName, fields))
+		g.typedefs = append(g.typedefs, newType(typeName, fields, schema.String()))
 	}
 	return typeName
 }
@@ -325,11 +365,13 @@ func (g *Generator) Write(w io.Writer) error {
 	}
 
 	data := struct {
+		WithEncoders      bool
 		PackageName       string
 		Imports           []string
 		ThirdPartyImports []string
 		Typedefs          []typedef
 	}{
+		WithEncoders:      g.encoders,
 		PackageName:       g.pkg,
 		Imports:           g.imports,
 		ThirdPartyImports: g.thirdPartyImports,
@@ -341,12 +383,14 @@ func (g *Generator) Write(w io.Writer) error {
 type typedef struct {
 	Name   string
 	Fields []field
+	Schema string
 }
 
-func newType(name string, fields []field) typedef {
+func newType(name string, fields []field, schema string) typedef {
 	return typedef{
 		Name:   name,
 		Fields: fields,
+		Schema: schema,
 	}
 }
 


Test patch:
diff --git a/cmd/avrogen/main_test.go b/cmd/avrogen/main_test.go
index ccf7673..677277d 100644
--- a/cmd/avrogen/main_test.go
+++ b/cmd/avrogen/main_test.go
@@ -117,6 +117,29 @@ func TestAvroGen_GeneratesSchemaWithFullname(t *testing.T) {
 	assert.Equal(t, want, got)
 }
 
+func TestAvroGen_GeneratesSchemaWithEncoders(t *testing.T) {
+	path, err := os.MkdirTemp("./", "avrogen")
+	require.NoError(t, err)
+	t.Cleanup(func() { _ = os.RemoveAll(path) })
+
+	file := filepath.Join(path, "test.go")
+	args := []string{"avrogen", "-pkg", "testpkg", "-o", file, "-encoders", "testdata/schema.avsc"}
+	gotCode := realMain(args, io.Discard)
+	require.Equal(t, 0, gotCode)
+
+	got, err := os.ReadFile(file)
+	require.NoError(t, err)
+
+	if *update {
+		err = os.WriteFile("testdata/golden_encoders.go", got, 0600)
+		require.NoError(t, err)
+	}
+
+	want, err := os.ReadFile("testdata/golden_encoders.go")
+	require.NoError(t, err)
+	assert.Equal(t, want, got)
+}
+
 func TestParseTags(t *testing.T) {
 	tests := []struct {
 		name string
diff --git a/cmd/avrogen/testdata/golden.go b/cmd/avrogen/testdata/golden.go
index 7f81cc5..2c00ae2 100644
--- a/cmd/avrogen/testdata/golden.go
+++ b/cmd/avrogen/testdata/golden.go
@@ -2,6 +2,7 @@ package testpkg
 
 // Code generated by avro/gen. DO NOT EDIT.
 
+// Test is a generated struct.
 type Test struct {
 	SomeString string `avro:"someString"`
 }
diff --git a/cmd/avrogen/testdata/golden_encoders.go b/cmd/avrogen/testdata/golden_encoders.go
new file mode 100644
index 0000000..c9b90de
--- /dev/null
+++ b/cmd/avrogen/testdata/golden_encoders.go
@@ -0,0 +1,32 @@
+package testpkg
+
+// Code generated by avro/gen. DO NOT EDIT.
+
+import (
+	"github.com/hamba/avro/v2"
+)
+
+// Test is a generated struct.
+type Test struct {
+	SomeString string `avro:"someString"`
+
+	schema avro.Schema
+}
+
+// Schema returns the schema for Test.
+func (o *Test) Schema() avro.Schema {
+	if o.schema == nil {
+		o.schema = avro.MustParse(`{"name":"a.b.test","type":"record","fields":[{"name":"someString","type":"string"}]}`)
+	}
+	return o.schema
+}
+
+// Unmarshal decodes b into the receiver.
+func (o *Test) Unmarshal(b []byte) error {
+	return avro.Unmarshal(o.Schema(), b, o)
+}
+
+// Marshal encodes the receiver.
+func (o *Test) Marshal() ([]byte, error) {
+	return avro.Marshal(o.Schema(), o)
+}
diff --git a/cmd/avrogen/testdata/golden_fullname.go b/cmd/avrogen/testdata/golden_fullname.go
index 6434b86..2f175b6 100644
--- a/cmd/avrogen/testdata/golden_fullname.go
+++ b/cmd/avrogen/testdata/golden_fullname.go
@@ -2,6 +2,7 @@ package testpkg
 
 // Code generated by avro/gen. DO NOT EDIT.
 
+// ABTest is a generated struct.
 type ABTest struct {
 	SomeString string `avro:"someString"`
 }
diff --git a/gen/gen_test.go b/gen/gen_test.go
index b21d618..738eb11 100644
--- a/gen/gen_test.go
+++ b/gen/gen_test.go
@@ -152,6 +152,23 @@ func TestStruct_GenFromRecordSchemaWithFullName(t *testing.T) {
 	assert.Equal(t, string(want), string(file))
 }
 
+func TestStruct_GenFromRecordSchemaWithEncoders(t *testing.T) {
+	schema, err := os.ReadFile("testdata/golden.avsc")
+	require.NoError(t, err)
+
+	gc := gen.Config{PackageName: "Something", Encoders: true}
+	file, _ := generate(t, string(schema), gc)
+
+	if *update {
+		err = os.WriteFile("testdata/golden_encoders.go", file, 0600)
+		require.NoError(t, err)
+	}
+
+	want, err := os.ReadFile("testdata/golden_encoders.go")
+	require.NoError(t, err)
+	assert.Equal(t, string(want), string(file))
+}
+
 func TestGenerator(t *testing.T) {
 	unionSchema, err := avro.ParseFiles("testdata/uniontype.avsc")
 	require.NoError(t, err)
diff --git a/gen/testdata/golden.go b/gen/testdata/golden.go
index 0dae757..b6f0c14 100644
--- a/gen/testdata/golden.go
+++ b/gen/testdata/golden.go
@@ -9,39 +9,48 @@ import (
 	"github.com/hamba/avro/v2"
 )
 
+// InnerRecord is a generated struct.
 type InnerRecord struct {
 	InnerJustBytes                   []byte    `avro:"innerJustBytes"`
 	InnerPrimitiveNullableArrayUnion *[]string `avro:"innerPrimitiveNullableArrayUnion"`
 }
 
+// RecordInMap is a generated struct.
 type RecordInMap struct {
 	Name string `avro:"name"`
 }
 
+// RecordInArray is a generated struct.
 type RecordInArray struct {
 	AString string `avro:"aString"`
 }
 
+// RecordInNullableUnion is a generated struct.
 type RecordInNullableUnion struct {
 	AString string `avro:"aString"`
 }
 
+// Record1InNonNullableUnion is a generated struct.
 type Record1InNonNullableUnion struct {
 	AString string `avro:"aString"`
 }
 
+// Record2InNonNullableUnion is a generated struct.
 type Record2InNonNullableUnion struct {
 	AString string `avro:"aString"`
 }
 
+// Record1InNullableUnion is a generated struct.
 type Record1InNullableUnion struct {
 	AString string `avro:"aString"`
 }
 
+// Record2InNullableUnion is a generated struct.
 type Record2InNullableUnion struct {
 	AString string `avro:"aString"`
 }
 
+// Test is a generated struct.
 type Test struct {
 	AString                         string                 `avro:"aString"`
 	ABoolean                        bool                   `avro:"aBoolean"`
diff --git a/gen/testdata/golden_encoders.go b/gen/testdata/golden_encoders.go
new file mode 100644
index 0000000..09e03f2
--- /dev/null
+++ b/gen/testdata/golden_encoders.go
@@ -0,0 +1,261 @@
+package something
+
+// Code generated by avro/gen. DO NOT EDIT.
+
+import (
+	"math/big"
+	"time"
+
+	"github.com/hamba/avro/v2"
+)
+
+// InnerRecord is a generated struct.
+type InnerRecord struct {
+	InnerJustBytes                   []byte    `avro:"innerJustBytes"`
+	InnerPrimitiveNullableArrayUnion *[]string `avro:"innerPrimitiveNullableArrayUnion"`
+
+	schema avro.Schema
+}
+
+// Schema returns the schema for InnerRecord.
+func (o *InnerRecord) Schema() avro.Schema {
+	if o.schema == nil {
+		o.schema = avro.MustParse(`{"name":"a.c.InnerRecord","type":"record","fields":[{"name":"innerJustBytes","type":"bytes"},{"name":"innerPrimitiveNullableArrayUnion","type":["null",{"type":"array","items":"string"}]}]}`)
+	}
+	return o.schema
+}
+
+// Unmarshal decodes b into the receiver.
+func (o *InnerRecord) Unmarshal(b []byte) error {
+	return avro.Unmarshal(o.Schema(), b, o)
+}
+
+// Marshal encodes the receiver.
+func (o *InnerRecord) Marshal() ([]byte, error) {
+	return avro.Marshal(o.Schema(), o)
+}
+
+// RecordInMap is a generated struct.
+type RecordInMap struct {
+	Name string `avro:"name"`
+
+	schema avro.Schema
+}
+
+// Schema returns the schema for RecordInMap.
+func (o *RecordInMap) Schema() avro.Schema {
+	if o.schema == nil {
+		o.schema = avro.MustParse(`{"name":"a.b.RecordInMap","type":"record","fields":[{"name":"name","type":"string"}]}`)
+	}
+	return o.schema
+}
+
+// Unmarshal decodes b into the receiver.
+func (o *RecordInMap) Unmarshal(b []byte) error {
+	return avro.Unmarshal(o.Schema(), b, o)
+}
+
+// Marshal encodes the receiver.
+func (o *RecordInMap) Marshal() ([]byte, error) {
+	return avro.Marshal(o.Schema(), o)
+}
+
+// RecordInArray is a generated struct.
+type RecordInArray struct {
+	AString string `avro:"aString"`
+
+	schema avro.Schema
+}
+
+// Schema returns the schema for RecordInArray.
+func (o *RecordInArray) Schema() avro.Schema {
+	if o.schema == nil {
+		o.schema = avro.MustParse(`{"name":"a.b.recordInArray","type":"record","fields":[{"name":"aString","type":"string"}]}`)
+	}
+	return o.schema
+}
+
+// Unmarshal decodes b into the receiver.
+func (o *RecordInArray) Unmarshal(b []byte) error {
+	return avro.Unmarshal(o.Schema(), b, o)
+}
+
+// Marshal encodes the receiver.
+func (o *RecordInArray) Marshal() ([]byte, error) {
+	return avro.Marshal(o.Schema(), o)
+}
+
+// RecordInNullableUnion is a generated struct.
+type RecordInNullableUnion struct {
+	AString string `avro:"aString"`
+
+	schema avro.Schema
+}
+
+// Schema returns the schema for RecordInNullableUnion.
+func (o *RecordInNullableUnion) Schema() avro.Schema {
+	if o.schema == nil {
+		o.schema = avro.MustParse(`{"name":"a.b.recordInNullableUnion","type":"record","fields":[{"name":"aString","type":"string"}]}`)
+	}
+	return o.schema
+}
+
+// Unmarshal decodes b into the receiver.
+func (o *RecordInNullableUnion) Unmarshal(b []byte) error {
+	return avro.Unmarshal(o.Schema(), b, o)
+}
+
+// Marshal encodes the receiver.
+func (o *RecordInNullableUnion) Marshal() ([]byte, error) {
+	return avro.Marshal(o.Schema(), o)
+}
+
+// Record1InNonNullableUnion is a generated struct.
+type Record1InNonNullableUnion struct {
+	AString string `avro:"aString"`
+
+	schema avro.Schema
+}
+
+// Schema returns the schema for Record1InNonNullableUnion.
+func (o *Record1InNonNullableUnion) Schema() avro.Schema {
+	if o.schema == nil {
+		o.schema = avro.MustParse(`{"name":"a.b.record1InNonNullableUnion","type":"record","fields":[{"name":"aString","type":"string"}]}`)
+	}
+	return o.schema
+}
+
+// Unmarshal decodes b into the receiver.
+func (o *Record1InNonNullableUnion) Unmarshal(b []byte) error {
+	return avro.Unmarshal(o.Schema(), b, o)
+}
+
+// Marshal encodes the receiver.
+func (o *Record1InNonNullableUnion) Marshal() ([]byte, error) {
+	return avro.Marshal(o.Schema(), o)
+}
+
+// Record2InNonNullableUnion is a generated struct.
+type Record2InNonNullableUnion struct {
+	AString string `avro:"aString"`
+
+	schema avro.Schema
+}
+
+// Schema returns the schema for Record2InNonNullableUnion.
+func (o *Record2InNonNullableUnion) Schema() avro.Schema {
+	if o.schema == nil {
+		o.schema = avro.MustParse(`{"name":"a.b.record2InNonNullableUnion","type":"record","fields":[{"name":"aString","type":"string"}]}`)
+	}
+	return o.schema
+}
+
+// Unmarshal decodes b into the receiver.
+func (o *Record2InNonNullableUnion) Unmarshal(b []byte) error {
+	return avro.Unmarshal(o.Schema(), b, o)
+}
+
+// Marshal encodes the receiver.
+func (o *Record2InNonNullableUnion) Marshal() ([]byte, error) {
+	return avro.Marshal(o.Schema(), o)
+}
+
+// Record1InNullableUnion is a generated struct.
+type Record1InNullableUnion struct {
+	AString string `avro:"aString"`
+
+	schema avro.Schema
+}
+
+// Schema returns the schema for Record1InNullableUnion.
+func (o *Record1InNullableUnion) Schema() avro.Schema {
+	if o.schema == nil {
+		o.schema = avro.MustParse(`{"name":"a.b.record1InNullableUnion","type":"record","fields":[{"name":"aString","type":"string"}]}`)
+	}
+	return o.schema
+}
+
+// Unmarshal decodes b into the receiver.
+func (o *Record1InNullableUnion) Unmarshal(b []byte) error {
+	return avro.Unmarshal(o.Schema(), b, o)
+}
+
+// Marshal encodes the receiver.
+func (o *Record1InNullableUnion) Marshal() ([]byte, error) {
+	return avro.Marshal(o.Schema(), o)
+}
+
+// Record2InNullableUnion is a generated struct.
+type Record2InNullableUnion struct {
+	AString string `avro:"aString"`
+
+	schema avro.Schema
+}
+
+// Schema returns the schema for Record2InNullableUnion.
+func (o *Record2InNullableUnion) Schema() avro.Schema {
+	if o.schema == nil {
+		o.schema = avro.MustParse(`{"name":"a.b.record2InNullableUnion","type":"record","fields":[{"name":"aString","type":"string"}]}`)
+	}
+	return o.schema
+}
+
+// Unmarshal decodes b into the receiver.
+func (o *Record2InNullableUnion) Unmarshal(b []byte) error {
+	return avro.Unmarshal(o.Schema(), b, o)
+}
+
+// Marshal encodes the receiver.
+func (o *Record2InNullableUnion) Marshal() ([]byte, error) {
+	return avro.Marshal(o.Schema(), o)
+}
+
+// Test is a generated struct.
+type Test struct {
+	AString                         string                 `avro:"aString"`
+	ABoolean                        bool                   `avro:"aBoolean"`
+	AnInt                           int                    `avro:"anInt"`
+	AFloat                          float32                `avro:"aFloat"`
+	ADouble                         float64                `avro:"aDouble"`
+	ALong                           int64                  `avro:"aLong"`
+	JustBytes                       []byte                 `avro:"justBytes"`
+	PrimitiveNullableArrayUnion     *[]string              `avro:"primitiveNullableArrayUnion"`
+	InnerRecord                     InnerRecord            `avro:"innerRecord"`
+	AnEnum                          string                 `avro:"anEnum"`
+	AFixed                          [7]byte                `avro:"aFixed"`
+	ALogicalFixed                   avro.LogicalDuration   `avro:"aLogicalFixed"`
+	AnotherLogicalFixed             avro.LogicalDuration   `avro:"anotherLogicalFixed"`
+	MapOfStrings                    map[string]string      `avro:"mapOfStrings"`
+	MapOfRecords                    map[string]RecordInMap `avro:"mapOfRecords"`
+	ADate                           time.Time              `avro:"aDate"`
+	ADuration                       time.Duration          `avro:"aDuration"`
+	ALongTimeMicros                 time.Duration          `avro:"aLongTimeMicros"`
+	ALongTimestampMillis            time.Time              `avro:"aLongTimestampMillis"`
+	ALongTimestampMicro             time.Time              `avro:"aLongTimestampMicro"`
+	ABytesDecimal                   *big.Rat               `avro:"aBytesDecimal"`
+	ARecordArray                    []RecordInArray        `avro:"aRecordArray"`
+	NullableRecordUnion             *RecordInNullableUnion `avro:"nullableRecordUnion"`
+	NonNullableRecordUnion          any                    `avro:"nonNullableRecordUnion"`
+	NullableRecordUnionWith3Options any                    `avro:"nullableRecordUnionWith3Options"`
+	Ref                             Record2InNullableUnion `avro:"ref"`
+
+	schema avro.Schema
+}
+
+// Schema returns the schema for Test.
+func (o *Test) Schema() avro.Schema {
+	if o.schema == nil {
+		o.schema = avro.MustParse(`{"name":"a.b.test","type":"record","fields":[{"name":"aString","type":"string"},{"name":"aBoolean","type":"boolean"},{"name":"anInt","type":"int"},{"name":"aFloat","type":"float"},{"name":"aDouble","type":"double"},{"name":"aLong","type":"long"},{"name":"justBytes","type":"bytes"},{"name":"primitiveNullableArrayUnion","type":["null",{"type":"array","items":"string"}]},{"name":"innerRecord","type":{"name":"a.c.InnerRecord","type":"record","fields":[{"name":"innerJustBytes","type":"bytes"},{"name":"innerPrimitiveNullableArrayUnion","type":["null",{"type":"array","items":"string"}]}]}},{"name":"anEnum","type":{"name":"a.b.Cards","type":"enum","symbols":["SPADES","HEARTS","DIAMONDS","CLUBS"]}},{"name":"aFixed","type":{"name":"a.b.fixedField","type":"fixed","size":7}},{"name":"aLogicalFixed","type":{"name":"a.b.logicalDuration","type":"fixed","size":12,"logicalType":"duration"}},{"name":"anotherLogicalFixed","type":{"name":"a.b.logicalDuration","type":"fixed","size":12,"logicalType":"duration"}},{"name":"mapOfStrings","type":{"type":"map","values":"string"}},{"name":"mapOfRecords","type":{"type":"map","values":{"name":"a.b.RecordInMap","type":"record","fields":[{"name":"name","type":"string"}]}}},{"name":"aDate","type":{"type":"int","logicalType":"date"}},{"name":"aDuration","type":{"type":"int","logicalType":"time-millis"}},{"name":"aLongTimeMicros","type":{"type":"long","logicalType":"time-micros"}},{"name":"aLongTimestampMillis","type":{"type":"long","logicalType":"timestamp-millis"}},{"name":"aLongTimestampMicro","type":{"type":"long","logicalType":"timestamp-micros"}},{"name":"aBytesDecimal","type":{"type":"bytes","logicalType":"decimal","precision":4,"scale":2}},{"name":"aRecordArray","type":{"type":"array","items":{"name":"a.b.recordInArray","type":"record","fields":[{"name":"aString","type":"string"}]}}},{"name":"nullableRecordUnion","type":["null",{"name":"a.b.recordInNullableUnion","type":"record","fields":[{"name":"aString","type":"string"}]}]},{"name":"nonNullableRecordUnion","type":[{"name":"a.b.record1InNonNullableUnion","type":"record","fields":[{"name":"aString","type":"string"}]},{"name":"a.b.record2InNonNullableUnion","type":"record","fields":[{"name":"aString","type":"string"}]}]},{"name":"nullableRecordUnionWith3Options","type":["null",{"name":"a.b.record1InNullableUnion","type":"record","fields":[{"name":"aString","type":"string"}]},{"name":"a.b.record2InNullableUnion","type":"record","fields":[{"name":"aString","type":"string"}]}]},{"name":"ref","type":"a.b.record2InNullableUnion"}]}`)
+	}
+	return o.schema
+}
+
+// Unmarshal decodes b into the receiver.
+func (o *Test) Unmarshal(b []byte) error {
+	return avro.Unmarshal(o.Schema(), b, o)
+}
+
+// Marshal encodes the receiver.
+func (o *Test) Marshal() ([]byte, error) {
+	return avro.Marshal(o.Schema(), o)
+}
diff --git a/gen/testdata/golden_fullname.go b/gen/testdata/golden_fullname.go
index 54f509f..e572449 100644
--- a/gen/testdata/golden_fullname.go
+++ b/gen/testdata/golden_fullname.go
@@ -9,39 +9,48 @@ import (
 	"github.com/hamba/avro/v2"
 )
 
+// ACInnerRecord is a generated struct.
 type ACInnerRecord struct {
 	InnerJustBytes                   []byte    `avro:"innerJustBytes"`
 	InnerPrimitiveNullableArrayUnion *[]string `avro:"innerPrimitiveNullableArrayUnion"`
 }
 
+// ABRecordInMap is a generated struct.
 type ABRecordInMap struct {
 	Name string `avro:"name"`
 }
 
+// ABRecordInArray is a generated struct.
 type ABRecordInArray struct {
 	AString string `avro:"aString"`
 }
 
+// ABRecordInNullableUnion is a generated struct.
 type ABRecordInNullableUnion struct {
 	AString string `avro:"aString"`
 }
 
+// ABRecord1InNonNullableUnion is a generated struct.
 type ABRecord1InNonNullableUnion struct {
 	AString string `avro:"aString"`
 }
 
+// ABRecord2InNonNullableUnion is a generated struct.
 type ABRecord2InNonNullableUnion struct {
 	AString string `avro:"aString"`
 }
 
+// ABRecord1InNullableUnion is a generated struct.
 type ABRecord1InNullableUnion struct {
 	AString string `avro:"aString"`
 }
 
+// ABRecord2InNullableUnion is a generated struct.
 type ABRecord2InNullableUnion struct {
 	AString string `avro:"aString"`
 }
 
+// ABTest is a generated struct.
 type ABTest struct {
 	AString                         string                   `avro:"aString"`
 	ABoolean                        bool                     `avro:"aBoolean"`
diff --git a/gen/testdata/golden_multiple.go b/gen/testdata/golden_multiple.go
index 9c8f31d..34c3f08 100644
--- a/gen/testdata/golden_multiple.go
+++ b/gen/testdata/golden_multiple.go
@@ -2,11 +2,13 @@ package something
 
 // Code generated by avro/gen. DO NOT EDIT.
 
+// TestUnionType is a generated struct.
 type TestUnionType struct {
 	Field1 int64 `avro:"Field1"`
 	Field2 int   `avro:"Field2"`
 }
 
+// TestMain is a generated struct.
 type TestMain struct {
 	TestUnion *TestUnionType `avro:"TestUnion"`
 }