Repository: hamba/avro
Pull request #9 (2019-04-04 09:51:15)

Schema Refactor


Patch:
diff --git a/README.md b/README.md
index 27eadae..1c461ba 100644
--- a/README.md
+++ b/README.md
@@ -112,8 +112,5 @@ Always benchmark with your own workload. The result depends heavily on the data
 ## TODO
 
 * Logical Types
-* Schema
-    * Refactor parsing to be cleaner
-    * Better schema validation
-    * Aliases
+* Aliases
 * Avro Textual Form?
diff --git a/codec_record.go b/codec_record.go
index ef8c49a..fb9a72a 100644
--- a/codec_record.go
+++ b/codec_record.go
@@ -126,7 +126,7 @@ func encoderOfStruct(cfg *frozenConfig, schema Schema, typ reflect2.Type) ValEnc
 				}
 
 				// In all other cases, this is a required field
-				return &errorEncoder{err: fmt.Errorf("avro: record %s is missing required field %s", rec.Name(), field.Name())}
+				return &errorEncoder{err: fmt.Errorf("avro: record %s is missing required field %s", rec.FullName(), field.Name())}
 			}
 
 			defaultType := reflect2.TypeOf(field.Default())
diff --git a/codec_union.go b/codec_union.go
index 35c67a2..11b7f6e 100644
--- a/codec_union.go
+++ b/codec_union.go
@@ -91,7 +91,7 @@ func (d *mapUnionDecoder) Decode(ptr unsafe.Pointer, r *Reader) {
 
 	key := string(resSchema.Type())
 	if n, ok := resSchema.(NamedSchema); ok {
-		key = n.Name()
+		key = n.FullName()
 	}
 	keyPtr := reflect2.PtrOf(key)
 
@@ -180,7 +180,7 @@ func (d *unionTypedDecoder) Decode(ptr unsafe.Pointer, r *Reader) {
 
 	key := string(schema.Type())
 	if n, ok := schema.(NamedSchema); ok {
-		key = n.Name()
+		key = n.FullName()
 	}
 
 	if err := union.SetType(key); err != nil {
diff --git a/ocf/codec.go b/ocf/codec.go
index b080059..d681060 100644
--- a/ocf/codec.go
+++ b/ocf/codec.go
@@ -12,8 +12,10 @@ import (
 	"github.com/golang/snappy"
 )
 
+// CodecName represents a compression codec name.
 type CodecName string
 
+// Supported compression codecs.
 const (
 	Null    CodecName = "null"
 	Deflate CodecName = "deflate"
@@ -36,23 +38,31 @@ func resolveCodec(name CodecName) (Codec, error) {
 	}
 }
 
+// Codec represents a compression codec.
 type Codec interface {
+	// Decode decodes the given bytes.
 	Decode([]byte) ([]byte, error)
+	// Encode encodes the given bytes.
 	Encode([]byte) []byte
 }
 
+// NullCodec is a no op codec.
 type NullCodec struct{}
 
+// Decode decodes the given bytes.
 func (*NullCodec) Decode(b []byte) ([]byte, error) {
 	return b, nil
 }
 
+// Encode encodes the given bytes.
 func (*NullCodec) Encode(b []byte) []byte {
 	return b
 }
 
+// DeflateCodec is a flate compression codec.
 type DeflateCodec struct{}
 
+// Decode decodes the given bytes.
 func (*DeflateCodec) Decode(b []byte) ([]byte, error) {
 	r := flate.NewReader(bytes.NewBuffer(b))
 	data, err := ioutil.ReadAll(r)
@@ -65,6 +75,7 @@ func (*DeflateCodec) Decode(b []byte) ([]byte, error) {
 	return data, nil
 }
 
+// Encode encodes the given bytes.
 func (*DeflateCodec) Encode(b []byte) []byte {
 	data := bytes.NewBuffer(make([]byte, 0, len(b)))
 
@@ -75,8 +86,10 @@ func (*DeflateCodec) Encode(b []byte) []byte {
 	return data.Bytes()
 }
 
+// SnappyCodec is a snappy compression codec.
 type SnappyCodec struct{}
 
+// Decode decodes the given bytes.
 func (*SnappyCodec) Decode(b []byte) ([]byte, error) {
 	l := len(b)
 	if l < 5 {
@@ -96,6 +109,7 @@ func (*SnappyCodec) Decode(b []byte) ([]byte, error) {
 	return dst, nil
 }
 
+// Encode encodes the given bytes.
 func (*SnappyCodec) Encode(b []byte) []byte {
 	dst := snappy.Encode(nil, b)
 
diff --git a/ocf/ocf.go b/ocf/ocf.go
index 4da1586..166190b 100644
--- a/ocf/ocf.go
+++ b/ocf/ocf.go
@@ -1,5 +1,5 @@
 /*
-Package container implements encoding and decoding of Avro Object Container Files as defined by the Avro specification.
+Package ocf implements encoding and decoding of Avro Object Container Files as defined by the Avro specification.
 
 See the Avro specification for an understanding of Avro: http://avro.apache.org/docs/current/
 
diff --git a/reader_generic.go b/reader_generic.go
index cd01ef2..08b7d75 100644
--- a/reader_generic.go
+++ b/reader_generic.go
@@ -81,7 +81,7 @@ func (r *Reader) ReadNext(schema Schema) interface{} {
 
 		key := string(schema.Type())
 		if n, ok := schema.(NamedSchema); ok {
-			key = n.Name()
+			key = n.FullName()
 		}
 
 		obj := map[string]interface{}{}
diff --git a/schema.go b/schema.go
index 4501e5f..173ddf2 100644
--- a/schema.go
+++ b/schema.go
@@ -2,7 +2,10 @@ package avro
 
 import (
 	"crypto/sha256"
+	"errors"
+	"fmt"
 	"strconv"
+	"strings"
 
 	"github.com/modern-go/concurrent"
 )
@@ -60,7 +63,7 @@ func (s Schemas) Get(name string) (Schema, int) {
 			return schema, i
 		}
 
-		if namedSchema, ok := schema.(NamedSchema); ok && namedSchema.Name() == name {
+		if namedSchema, ok := schema.(NamedSchema); ok && namedSchema.FullName() == name {
 			return schema, i
 		}
 	}
@@ -84,15 +87,80 @@ type Schema interface {
 type NamedSchema interface {
 	Schema
 
-	// Name returns the fill name of the schema.
+	// Name returns the name of the schema.
 	Name() string
+
+	// Namespace returns the namespace of a schema.
+	Namespace() string
+
+	// FullName returns the full qualified name of a schema.
+	FullName() string
+}
+
+type name struct {
+	name  string
+	space string
+	full  string
+}
+
+func newName(n, s string) (name, error) {
+	if idx := strings.LastIndexByte(n, '.'); idx > -1 {
+		s = n[:idx]
+		n = n[idx+1:]
+	}
+
+	full := n
+	if s != "" {
+		full = s + "." + n
+	}
+
+	for _, part := range strings.Split(full, ".") {
+		if err := validateName(part); err != nil {
+			return name{}, err
+		}
+	}
+
+	return name{
+		name:  n,
+		space: s,
+		full:  full,
+	}, nil
+}
+
+// Name returns the name of a schema.
+func (n name) Name() string {
+	return n.name
+}
+
+// Namespace returns the namespace of a schema.
+func (n name) Namespace() string {
+	return n.space
+}
+
+// FullName returns the full qualified name of a schema.
+func (n name) FullName() string {
+	return n.full
+}
+
+type fingerprinter struct {
+	fingerprint [32]byte
+}
+
+// Fingerprint returns the SHA256 fingerprint of the schema.
+func (f *fingerprinter) Fingerprint(stringer fmt.Stringer) [32]byte {
+	if f.fingerprint != emptyFgpt {
+		return f.fingerprint
+	}
+
+	f.fingerprint = sha256.Sum256([]byte(stringer.String()))
+	return f.fingerprint
 }
 
 // PrimitiveSchema is an Avro primitive type schema.
 type PrimitiveSchema struct {
-	typ Type
+	fingerprinter
 
-	fingerprint [32]byte
+	typ Type
 }
 
 // NewPrimitiveSchema creates a new PrimitiveSchema.
@@ -114,20 +182,27 @@ func (s *PrimitiveSchema) String() string {
 
 // Fingerprint returns the SHA256 fingerprint of the schema.
 func (s *PrimitiveSchema) Fingerprint() [32]byte {
-	if s.fingerprint != emptyFgpt {
-		return s.fingerprint
-	}
-
-	s.fingerprint = sha256.Sum256([]byte(s.String()))
-	return s.fingerprint
+	return s.fingerprinter.Fingerprint(s)
 }
 
 // RecordSchema is an Avro record type schema.
 type RecordSchema struct {
-	name   string
+	name
+	fingerprinter
+
 	fields []*Field
+}
 
-	fingerprint [32]byte
+// NewRecordSchema creates a new record schema instance.
+func NewRecordSchema(name, space string) (*RecordSchema, error) {
+	n, err := newName(name, space)
+	if err != nil {
+		return nil, err
+	}
+
+	return &RecordSchema{
+		name: n,
+	}, nil
 }
 
 // Type returns the type of the schema.
@@ -135,9 +210,12 @@ func (s *RecordSchema) Type() Type {
 	return Record
 }
 
-// Name implements the NamedSchema interface.
-func (s *RecordSchema) Name() string {
-	return s.name
+// AddField adds a field to the record.
+func (s *RecordSchema) AddField(field *Field) {
+	// Clear the fingerprint cache
+	s.fingerprint = emptyFgpt
+
+	s.fields = append(s.fields, field)
 }
 
 // Fields returns the fields of a record.
@@ -155,17 +233,12 @@ func (s *RecordSchema) String() string {
 		fields = fields[:len(fields)-1]
 	}
 
-	return `{"name":"` + s.name + `","type":"record","fields":[` + fields + `]}`
+	return `{"name":"` + s.FullName() + `","type":"record","fields":[` + fields + `]}`
 }
 
 // Fingerprint returns the SHA256 fingerprint of the schema.
 func (s *RecordSchema) Fingerprint() [32]byte {
-	if s.fingerprint != emptyFgpt {
-		return s.fingerprint
-	}
-
-	s.fingerprint = sha256.Sum256([]byte(s.String()))
-	return s.fingerprint
+	return s.fingerprinter.Fingerprint(s)
 }
 
 // Field is an Avro record type field.
@@ -175,6 +248,24 @@ type Field struct {
 	def  interface{}
 }
 
+// NewField creates a new field instance.
+func NewField(name string, typ Schema, def interface{}) (*Field, error) {
+	if err := validateName(name); err != nil {
+		return nil, err
+	}
+
+	def, err := validateDefault(name, typ, def)
+	if err != nil {
+		return nil, err
+	}
+
+	return &Field{
+		name: name,
+		typ:  typ,
+		def:  def,
+	}, nil
+}
+
 // Name returns the name of a field.
 func (s *Field) Name() string {
 	return s.name
@@ -199,11 +290,32 @@ func (s *Field) String() string {
 
 // EnumSchema is an Avro enum type schema.
 type EnumSchema struct {
-	name    string
+	name
+	fingerprinter
+
 	symbols []string
+}
 
-	canonical   string
-	fingerprint [32]byte
+// NewEnumSchema creates a new enum schema instance.
+func NewEnumSchema(name, namespace string, symbols []string) (*EnumSchema, error) {
+	n, err := newName(name, namespace)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(symbols) == 0 {
+		return nil, errors.New("avro: enum must have a non-empty array of symbols")
+	}
+	for _, symbol := range symbols {
+		if err := validateName(symbol); err != nil {
+			return nil, fmt.Errorf("avro: invalid symnol %s", symbol)
+		}
+	}
+
+	return &EnumSchema{
+		name:    n,
+		symbols: symbols,
+	}, nil
 }
 
 // Type returns the type of the schema.
@@ -211,11 +323,6 @@ func (s *EnumSchema) Type() Type {
 	return Enum
 }
 
-// Name implements the NamedSchema interface.
-func (s *EnumSchema) Name() string {
-	return s.name
-}
-
 // Symbols returns the symbols of an enum.
 func (s *EnumSchema) Symbols() []string {
 	return s.symbols
@@ -223,10 +330,6 @@ func (s *EnumSchema) Symbols() []string {
 
 // String returns the canonical form of the schema.
 func (s *EnumSchema) String() string {
-	if s.canonical != "" {
-		return s.canonical
-	}
-
 	symbols := ""
 	for _, sym := range s.symbols {
 		symbols += `"` + sym + `",`
@@ -235,25 +338,26 @@ func (s *EnumSchema) String() string {
 		symbols = symbols[:len(symbols)-1]
 	}
 
-	s.canonical = `{"name":"` + s.name + `","type":"enum","symbols":[` + symbols + `]}`
-	return s.canonical
+	return `{"name":"` + s.FullName() + `","type":"enum","symbols":[` + symbols + `]}`
 }
 
 // Fingerprint returns the SHA256 fingerprint of the schema.
 func (s *EnumSchema) Fingerprint() [32]byte {
-	if s.fingerprint != emptyFgpt {
-		return s.fingerprint
-	}
-
-	s.fingerprint = sha256.Sum256([]byte(s.String()))
-	return s.fingerprint
+	return s.fingerprinter.Fingerprint(s)
 }
 
 // ArraySchema is an Avro array type schema.
 type ArraySchema struct {
+	fingerprinter
+
 	items Schema
+}
 
-	fingerprint [32]byte
+// NewArraySchema creates an array schema instance.
+func NewArraySchema(items Schema) *ArraySchema {
+	return &ArraySchema{
+		items: items,
+	}
 }
 
 // Type returns the type of the schema.
@@ -273,19 +377,21 @@ func (s *ArraySchema) String() string {
 
 // Fingerprint returns the SHA256 fingerprint of the schema.
 func (s *ArraySchema) Fingerprint() [32]byte {
-	if s.fingerprint != emptyFgpt {
-		return s.fingerprint
-	}
-
-	s.fingerprint = sha256.Sum256([]byte(s.String()))
-	return s.fingerprint
+	return s.fingerprinter.Fingerprint(s)
 }
 
 // MapSchema is an Avro map type schema.
 type MapSchema struct {
+	fingerprinter
+
 	values Schema
+}
 
-	fingerprint [32]byte
+// NewMapSchema creates a map schema instance.
+func NewMapSchema(values Schema) *MapSchema {
+	return &MapSchema{
+		values: values,
+	}
 }
 
 // Type returns the type of the schema.
@@ -300,12 +406,7 @@ func (s *MapSchema) Values() Schema {
 
 // Fingerprint returns the SHA256 fingerprint of the schema.
 func (s *MapSchema) Fingerprint() [32]byte {
-	if s.fingerprint != emptyFgpt {
-		return s.fingerprint
-	}
-
-	s.fingerprint = sha256.Sum256([]byte(s.String()))
-	return s.fingerprint
+	return s.fingerprinter.Fingerprint(s)
 }
 
 // String returns the canonical form of the schema.
@@ -315,9 +416,33 @@ func (s *MapSchema) String() string {
 
 // UnionSchema is an Avro union type schema.
 type UnionSchema struct {
+	fingerprinter
+
 	types Schemas
+}
 
-	fingerprint [32]byte
+// NewUnionSchema creates a union schema instance.
+func NewUnionSchema(types []Schema) (*UnionSchema, error) {
+	seen := map[string]bool{}
+	for _, schema := range types {
+		if schema.Type() == Union {
+			return nil, errors.New("avro: union type cannot be a union")
+		}
+
+		strType := string(schema.Type())
+		if named, ok := schema.(NamedSchema); ok {
+			strType = named.FullName()
+		}
+
+		if seen[strType] {
+			return nil, errors.New("avro: union type must be unique")
+		}
+		seen[strType] = true
+	}
+
+	return &UnionSchema{
+		types: Schemas(types),
+	}, nil
 }
 
 // Type returns the type of the schema.
@@ -354,20 +479,28 @@ func (s *UnionSchema) String() string {
 
 // Fingerprint returns the SHA256 fingerprint of the schema.
 func (s *UnionSchema) Fingerprint() [32]byte {
-	if s.fingerprint != emptyFgpt {
-		return s.fingerprint
-	}
-
-	s.fingerprint = sha256.Sum256([]byte(s.String()))
-	return s.fingerprint
+	return s.fingerprinter.Fingerprint(s)
 }
 
 // FixedSchema is an Avro fixed type schema.
 type FixedSchema struct {
-	name string
+	name
+	fingerprinter
+
 	size int
+}
 
-	fingerprint [32]byte
+// NewFixedSchema creates a new fixed schema instance.
+func NewFixedSchema(name, namespace string, size int) (*FixedSchema, error) {
+	n, err := newName(name, namespace)
+	if err != nil {
+		return nil, err
+	}
+
+	return &FixedSchema{
+		name: n,
+		size: size,
+	}, nil
 }
 
 // Type returns the type of the schema.
@@ -375,11 +508,6 @@ func (s *FixedSchema) Type() Type {
 	return Fixed
 }
 
-// Name implements the NamedSchema interface.
-func (s *FixedSchema) Name() string {
-	return s.name
-}
-
 // Size returns the number of bytes of the fixed schema.
 func (s *FixedSchema) Size() int {
 	return s.size
@@ -388,17 +516,12 @@ func (s *FixedSchema) Size() int {
 // String returns the canonical form of the schema.
 func (s *FixedSchema) String() string {
 	size := strconv.Itoa(s.size)
-	return `{"name":"` + s.name + `","type":"fixed","size":` + size + `}`
+	return `{"name":"` + s.FullName() + `","type":"fixed","size":` + size + `}`
 }
 
 // Fingerprint returns the SHA256 fingerprint of the schema.
 func (s *FixedSchema) Fingerprint() [32]byte {
-	if s.fingerprint != emptyFgpt {
-		return s.fingerprint
-	}
-
-	s.fingerprint = sha256.Sum256([]byte(s.String()))
-	return s.fingerprint
+	return s.fingerprinter.Fingerprint(s)
 }
 
 // NullSchema is an Avro null type schema.
@@ -424,6 +547,13 @@ type RefSchema struct {
 	actual NamedSchema
 }
 
+// NewRefSchema creates a ref schema instance.
+func NewRefSchema(schema NamedSchema) *RefSchema {
+	return &RefSchema{
+		actual: schema,
+	}
+}
+
 // Type returns the type of the schema.
 func (s *RefSchema) Type() Type {
 	return Ref
@@ -436,10 +566,165 @@ func (s *RefSchema) Schema() Schema {
 
 // String returns the canonical form of the schema.
 func (s *RefSchema) String() string {
-	return `"` + s.actual.Name() + `"`
+	return `"` + s.actual.FullName() + `"`
 }
 
 // Fingerprint returns the SHA256 fingerprint of the schema.
 func (s *RefSchema) Fingerprint() [32]byte {
 	return s.actual.Fingerprint()
 }
+
+func invalidNameFirstChar(r rune) bool {
+	return (r < 'A' || r > 'Z') && (r < 'a' || r > 'z') && r != '_'
+}
+
+func invalidNameOtherChar(r rune) bool {
+	return invalidNameFirstChar(r) && (r < '0' || r > '9')
+}
+
+func validateName(name string) error {
+	if len(name) == 0 {
+		return errors.New("avro: name must be a non-empty")
+	}
+
+	if strings.IndexFunc(name[:1], invalidNameFirstChar) > -1 {
+		return fmt.Errorf("avro: invalid name %s", name)
+	}
+	if strings.IndexFunc(name[1:], invalidNameOtherChar) > -1 {
+		return fmt.Errorf("avro: invalid name %s", name)
+	}
+
+	return nil
+}
+
+func validateDefault(name string, schema Schema, def interface{}) (interface{}, error) {
+	if def == nil {
+		return nil, nil
+	}
+
+	def, ok := isValidDefault(schema, def)
+	if !ok {
+		return nil, fmt.Errorf("avro: invalid default for field %s. %+v not a %s", name, def, schema.Type())
+	}
+
+	return def, nil
+}
+
+func isValidDefault(schema Schema, def interface{}) (interface{}, bool) {
+	switch schema.Type() {
+	case Null:
+		return nil, def == nil
+
+	case String, Bytes, Enum, Fixed:
+		if _, ok := def.(string); ok {
+			return def, true
+		}
+
+	case Boolean:
+		if _, ok := def.(bool); ok {
+			return def, true
+		}
+
+	case Int:
+		if i, ok := def.(int8); ok {
+			return int(i), true
+		}
+		if i, ok := def.(int16); ok {
+			return int(i), true
+		}
+		if i, ok := def.(int32); ok {
+			return int(i), true
+		}
+		if _, ok := def.(int); ok {
+			return def, true
+		}
+		if f, ok := def.(float64); ok {
+			return int(f), true
+		}
+
+	case Long:
+		if _, ok := def.(int64); ok {
+			return def, true
+		}
+		if f, ok := def.(float64); ok {
+			return int64(f), true
+		}
+
+	case Float:
+		if _, ok := def.(float32); ok {
+			return def, true
+		}
+		if f, ok := def.(float64); ok {
+			return float32(f), true
+		}
+
+	case Double:
+		if _, ok := def.(float64); ok {
+			return def, true
+		}
+
+	case Array:
+		arr, ok := def.([]interface{})
+		if !ok {
+			return nil, false
+		}
+
+		arrSchema := schema.(*ArraySchema)
+		for i, v := range arr {
+			v, ok := isValidDefault(arrSchema.Items(), v)
+			if !ok {
+				return nil, false
+			}
+			arr[i] = v
+		}
+
+		return arr, true
+
+	case Map:
+		m, ok := def.(map[string]interface{})
+		if !ok {
+			return nil, false
+		}
+
+		mapSchema := schema.(*MapSchema)
+		for k, v := range m {
+			v, ok := isValidDefault(mapSchema.Values(), v)
+			if !ok {
+				return nil, false
+			}
+
+			m[k] = v
+		}
+
+		return m, true
+
+	case Union:
+		unionSchema := schema.(*UnionSchema)
+		return isValidDefault(unionSchema.Types()[0], def)
+
+	case Record:
+		m, ok := def.(map[string]interface{})
+		if !ok {
+			return nil, false
+		}
+
+		recordSchema := schema.(*RecordSchema)
+		for _, field := range recordSchema.Fields() {
+			fieldDef := field.Default()
+			if newDef, ok := m[field.Name()]; ok {
+				fieldDef = newDef
+			}
+
+			v, ok := isValidDefault(field.Type(), fieldDef)
+			if !ok {
+				return nil, false
+			}
+
+			m[field.Name()] = v
+		}
+
+		return m, true
+	}
+
+	return nil, false
+}
diff --git a/schema_parse.go b/schema_parse.go
index 3cd8127..256ee78 100644
--- a/schema_parse.go
+++ b/schema_parse.go
@@ -4,14 +4,11 @@ import (
 	"errors"
 	"fmt"
 	"io/ioutil"
-	"regexp"
 	"strings"
 
 	"github.com/json-iterator/go"
 )
 
-var nameRegexp = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*?(\.[A-Za-z_][A-Za-z0-9_]*?)*$`)
-
 // Parse parses a schema string.
 func Parse(schema string) (Schema, error) {
 	var json interface{}
@@ -141,20 +138,20 @@ func parseRecord(namespace string, m map[string]interface{}) (Schema, error) {
 		return nil, errors.New("avro: record must have an array of fields")
 	}
 
-	rec := &RecordSchema{
-		name:   fullName(namespace, name),
-		fields: make([]*Field, len(fields)),
+	rec, err := NewRecordSchema(name, namespace)
+	if err != nil {
+		return nil, err
 	}
 
-	schemaConfig.addSchemaToCache(rec.name, &RefSchema{actual: rec})
+	schemaConfig.addSchemaToCache(rec.FullName(), NewRefSchema(rec))
 
-	for i, f := range fields {
+	for _, f := range fields {
 		field, err := parseField(namespace, f)
 		if err != nil {
 			return nil, err
 		}
 
-		rec.fields[i] = field
+		rec.AddField(field)
 	}
 
 	return rec, nil
@@ -180,26 +177,9 @@ func parseField(namespace string, v interface{}) (*Field, error) {
 		return nil, err
 	}
 
-	field := &Field{
-		name: name,
-		typ:  typ,
-		def:  m["default"],
-	}
-
-	// TODO: check default types
-	// if the type is a union, the default must be of the same type if the first type
-	// Sanitise the default
-	if f, ok := field.def.(float64); ok {
-		switch typ.Type() {
-		case Int:
-			field.def = int32(f)
-
-		case Long:
-			field.def = int64(f)
-
-		case Float:
-			field.def = float32(f)
-		}
+	field, err := NewField(name, typ, m["default"])
+	if err != nil {
+		return nil, err
 	}
 
 	return field, nil
@@ -226,23 +206,15 @@ func parseEnum(namespace string, m map[string]interface{}) (Schema, error) {
 			return nil, fmt.Errorf("avro: invalid symbol: %+v", sym)
 		}
 
-		if err := validateName(str); err != nil {
-			return nil, err
-		}
-
 		symbols[i] = str
 	}
 
-	if len(symbols) == 0 {
-		return nil, errors.New("avro: enum must have a non-empty array of symbols")
-	}
-
-	enum := &EnumSchema{
-		name:    fullName(namespace, name),
-		symbols: symbols,
+	enum, err := NewEnumSchema(name, namespace, symbols)
+	if err != nil {
+		return nil, err
 	}
 
-	schemaConfig.addSchemaToCache(enum.name, enum)
+	schemaConfig.addSchemaToCache(enum.FullName(), enum)
 
 	return enum, nil
 }
@@ -258,9 +230,7 @@ func parseArray(namespace string, m map[string]interface{}) (Schema, error) {
 		return nil, err
 	}
 
-	return &ArraySchema{
-		items: schema,
-	}, nil
+	return NewArraySchema(schema), nil
 }
 
 func parseMap(namespace string, m map[string]interface{}) (Schema, error) {
@@ -274,27 +244,20 @@ func parseMap(namespace string, m map[string]interface{}) (Schema, error) {
 		return nil, err
 	}
 
-	return &MapSchema{
-		values: schema,
-	}, nil
+	return NewMapSchema(schema), nil
 }
 
 func parseUnion(namespace string, v []interface{}) (Schema, error) {
-	types := make([]Schema, len(v))
-
 	var err error
+	types := make([]Schema, len(v))
 	for i := range v {
 		types[i], err = parseType(namespace, v[i])
 		if err != nil {
 			return nil, err
 		}
-
-		// TODO: types[i] cannot be a union
-		// No dup types, except if named and names are different
 	}
-	return &UnionSchema{
-		types: Schemas(types),
-	}, nil
+
+	return NewUnionSchema(types)
 }
 
 func parseFixed(namespace string, m map[string]interface{}) (Schema, error) {
@@ -311,12 +274,12 @@ func parseFixed(namespace string, m map[string]interface{}) (Schema, error) {
 		return nil, errors.New("avro: fixed must have a size")
 	}
 
-	fixed := &FixedSchema{
-		name: fullName(namespace, name),
-		size: int(size),
+	fixed, err := NewFixedSchema(name, namespace, int(size))
+	if err != nil {
+		return nil, err
 	}
 
-	schemaConfig.addSchemaToCache(fixed.name, fixed)
+	schemaConfig.addSchemaToCache(fixed.FullName(), fixed)
 
 	return fixed, nil
 }
@@ -335,14 +298,6 @@ func resolveName(m map[string]interface{}) (string, error) {
 		return "", errors.New("avro: name key required")
 	}
 
-	if name == "" {
-		return "", errors.New("avro: name must be a non-empty")
-	}
-
-	if err := validateName(name); err != nil {
-		return "", err
-	}
-
 	return name, nil
 }
 
@@ -360,25 +315,5 @@ func resolveFullName(m map[string]interface{}) (string, string, error) {
 		return "", "", errors.New("avro: namespace key must be non-empty or omitted")
 	}
 
-	if err := validateNamespace(namespace); err != nil {
-		return "", "", err
-	}
-
 	return name, namespace, nil
 }
-
-func validateName(name string) error {
-	if !nameRegexp.MatchString(name) {
-		return fmt.Errorf("avro: name is invalid: %s", name)
-	}
-
-	return nil
-}
-
-func validateNamespace(namespace string) error {
-	if !nameRegexp.MatchString(namespace) {
-		return fmt.Errorf("avro: namespace is invalid: %s", namespace)
-	}
-
-	return nil
-}


Test patch:
diff --git a/schema_internal_test.go b/schema_internal_test.go
new file mode 100644
index 0000000..eaeb112
--- /dev/null
+++ b/schema_internal_test.go
@@ -0,0 +1,298 @@
+package avro
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestName_NameAndNamespace(t *testing.T) {
+	n, _ := newName("bar", "foo")
+
+	assert.Equal(t, "bar", n.Name())
+	assert.Equal(t, "foo", n.Namespace())
+	assert.Equal(t, "foo.bar", n.FullName())
+}
+
+func TestName_QualifiedName(t *testing.T) {
+	n, _ := newName("foo.bar", "test")
+
+	assert.Equal(t, "bar", n.Name())
+	assert.Equal(t, "foo", n.Namespace())
+	assert.Equal(t, "foo.bar", n.FullName())
+}
+
+func TestName_InvalidNameFirstChar(t *testing.T) {
+	_, err := newName("+bar", "foo")
+
+	assert.Error(t, err)
+}
+
+func TestName_InvalidNameOtherChar(t *testing.T) {
+	_, err := newName("bar+", "foo")
+
+	assert.Error(t, err)
+}
+
+func TestName_InvalidNamespaceFirstChar(t *testing.T) {
+	_, err := newName("bar", "+foo")
+
+	assert.Error(t, err)
+}
+
+func TestName_InvalidNamespaceOtherChar(t *testing.T) {
+	_, err := newName("bar", "foo+")
+
+	assert.Error(t, err)
+}
+
+func TestIsValidDefault(t *testing.T) {
+	tests := []struct {
+		name     string
+		schemaFn func() Schema
+		def      interface{}
+		want     interface{}
+		wantOk   bool
+	}{
+
+		{
+			name: "Null",
+			schemaFn: func() Schema {
+				return &NullSchema{}
+			},
+			def:    nil,
+			want:   nil,
+			wantOk: true,
+		},
+		{
+			name: "Null Invalid Type",
+			schemaFn: func() Schema {
+				return &NullSchema{}
+			},
+			def:    "test",
+			wantOk: false,
+		},
+		{
+			name: "String",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(String)
+			},
+			def:    "test",
+			want:   "test",
+			wantOk: true,
+		},
+		{
+			name: "String Invalid Type",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(String)
+			},
+			def:    1,
+			wantOk: false,
+		},
+		{
+			name: "Bytes",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(Bytes)
+			},
+			def:    "test",
+			want:   "test",
+			wantOk: true,
+		},
+		{
+			name: "Bytes Invalid Type",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(Bytes)
+			},
+			def:    1,
+			wantOk: false,
+		},
+		{
+			name: "Enum",
+			schemaFn: func() Schema {
+				s, _ := NewEnumSchema("foo", "", []string{"BAR"})
+				return s
+			},
+			def:    "test",
+			want:   "test",
+			wantOk: true,
+		},
+		{
+			name: "Enum Invalid Type",
+			schemaFn: func() Schema {
+				s, _ := NewEnumSchema("foo", "", []string{"BAR"})
+				return s
+			},
+			def:    1,
+			wantOk: false,
+		},
+		{
+			name: "Fixed",
+			schemaFn: func() Schema {
+				s, _ := NewFixedSchema("foo", "", 1)
+				return s
+			},
+			def:    "test",
+			want:   "test",
+			wantOk: true,
+		},
+		{
+			name: "Fixed Invalid Type",
+			schemaFn: func() Schema {
+				s, _ := NewFixedSchema("foo", "", 1)
+				return s
+			},
+			def:    1,
+			wantOk: false,
+		},
+		{
+			name: "Boolean",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(Boolean)
+			},
+			def:    true,
+			want:   true,
+			wantOk: true,
+		},
+		{
+			name: "Boolean Invalid Type",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(Boolean)
+			},
+			def:    1,
+			wantOk: false,
+		},
+		{
+			name: "Int",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(Int)
+			},
+			def:    1,
+			want:   1,
+			wantOk: true,
+		},
+		{
+			name: "Int Int8",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(Int)
+			},
+			def:    int8(1),
+			want:   1,
+			wantOk: true,
+		},
+		{
+			name: "Int Int16",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(Int)
+			},
+			def:    int16(1),
+			want:   1,
+			wantOk: true,
+		},
+		{
+			name: "Int Int32",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(Int)
+			},
+			def:    int32(1),
+			want:   1,
+			wantOk: true,
+		},
+		{
+			name: "Int Float64",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(Int)
+			},
+			def:    float64(1),
+			want:   1,
+			wantOk: true,
+		},
+		{
+			name: "Int Invalid Type",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(Int)
+			},
+			def:    "test",
+			wantOk: false,
+		},
+		{
+			name: "Long",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(Long)
+			},
+			def:    int64(1),
+			want:   int64(1),
+			wantOk: true,
+		},
+		{
+			name: "Long Float64",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(Long)
+			},
+			def:    float64(1),
+			want:   int64(1),
+			wantOk: true,
+		},
+		{
+			name: "Long Invalid Type",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(Long)
+			},
+			def:    "test",
+			wantOk: false,
+		},
+		{
+			name: "Float",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(Float)
+			},
+			def:    float32(1),
+			want:   float32(1),
+			wantOk: true,
+		},
+		{
+			name: "Float Float64",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(Float)
+			},
+			def:    float64(1),
+			want:   float32(1),
+			wantOk: true,
+		},
+		{
+			name: "Float Invalid Type",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(Float)
+			},
+			def:    "test",
+			wantOk: false,
+		},
+		{
+			name: "Double",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(Double)
+			},
+			def:    float64(1),
+			want:   float64(1),
+			wantOk: true,
+		},
+		{
+			name: "Double Invalid Type",
+			schemaFn: func() Schema {
+				return NewPrimitiveSchema(Double)
+			},
+			def:    "test",
+			wantOk: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			got, ok := isValidDefault(tt.schemaFn(), tt.def)
+
+			assert.Equal(t, tt.wantOk, ok)
+			if ok {
+				assert.Equal(t, tt.want, got)
+			}
+		})
+	}
+}
diff --git a/schema_test.go b/schema_test.go
index 6669a7a..670b6c1 100644
--- a/schema_test.go
+++ b/schema_test.go
@@ -173,7 +173,12 @@ func TestRecordSchema(t *testing.T) {
 			wantErr: false,
 		},
 		{
-			name:    "Invalid Name",
+			name:    "Invalid Name First Char",
+			schema:  `{"type":"record", "name":"0test", "namespace": "org.apache.avro", "fields":[{"name": "field", "type": "int"}]}`,
+			wantErr: true,
+		},
+		{
+			name:    "Invalid Name Other Char",
 			schema:  `{"type":"record", "name":"test+", "namespace": "org.apache.avro", "fields":[{"name": "field", "type": "int"}]}`,
 			wantErr: true,
 		},
@@ -244,45 +249,109 @@ func TestRecordSchema(t *testing.T) {
 	}
 }
 
-func TestRecordSchema_Default(t *testing.T) {
+func TestRecordSchema_ValidatesDefault(t *testing.T) {
 	tests := []struct {
 		name   string
 		schema string
-		want   interface{}
+		wantErr   bool
 	}{
 		{
-			name:   "Normal",
-			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "field", "type": "string", "default": "test"}]}`,
-			want:   "test",
+			name:   "String",
+			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "a", "type": "string", "default": "test"}]}`,
+			wantErr:   false,
 		},
 		{
 			name:   "Int",
-			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "field", "type": "int", "default": 1}]}`,
-			want:   int32(1),
+			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "a", "type": "int", "default": 1}]}`,
+			wantErr:   false,
 		},
 		{
 			name:   "Long",
-			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "field", "type": "long", "default": 1}]}`,
-			want:   int64(1),
+			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "a", "type": "long", "default": 1}]}`,
+			wantErr:   false,
 		},
 		{
 			name:   "Float",
-			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "field", "type": "float", "default": 1}]}`,
-			want:   float32(1),
+			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "a", "type": "float", "default": 1}]}`,
+			wantErr:   false,
 		},
 		{
 			name:   "Double",
-			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "field", "type": "double", "default": 1}]}`,
-			want:   float64(1),
+			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "a", "type": "double", "default": 1}]}`,
+			wantErr:   false,
+		},
+		{
+			name:   "Array",
+			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "a", "type": {"type":"array", "items": "int"}, "default": [1,2]}]}`,
+			wantErr:   false,
+		},
+		{
+			name:   "Array Not Array",
+			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "a", "type": {"type":"array", "items": "int"}, "default": "test"}]}`,
+			wantErr:   true,
+		},
+		{
+			name:   "Array Invalid Type",
+			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "a", "type": {"type":"array", "items": "int"}, "default": ["test"]}]}`,
+			wantErr:   true,
+		},
+		{
+			name:   "Map",
+			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "a", "type": {"type":"map", "values": "int"}, "default": {"b": 1}}]}`,
+			wantErr:   false,
+		},
+		{
+			name:   "Map Not Map",
+			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "a", "type": {"type":"map", "values": "int"}, "default": "test"}]}`,
+			wantErr:   true,
+		},
+		{
+			name:   "Map Invalid Type",
+			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "a", "type": {"type":"map", "values": "int"}, "default": {"b": "test"}}]}`,
+			wantErr:   true,
+		},
+		{
+			name:   "Union",
+			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "a", "type": ["null", "string"], "default": null}]}`,
+			wantErr:   false,
+		},
+		{
+			name:   "Union Invalid Type",
+			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "a", "type": ["null", "string"], "default": "string"}]}`,
+			wantErr:   true,
+		},
+		{
+			name:   "Record",
+			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "a", "type": {"type":"record", "name": "test2", "fields":[{"name": "b", "type": "int"},{"name": "c", "type": "int", "default": 1}]}, "default": {"b": 1}}]}`,
+			wantErr:   false,
+		},
+		{
+			name:   "Record Not Map",
+			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "a", "type": {"type":"record", "name": "test2", "fields":[{"name": "b", "type": "int"},{"name": "c", "type": "int", "default": 1}]}, "default": "test"}]}`,
+			wantErr:   true,
+		},
+		{
+			name:   "Record Invalid Type",
+			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "a", "type": {"type":"record", "name": "test2", "fields":[{"name": "b", "type": "int"},{"name": "c", "type": "int", "default": 1}]}, "default": {"b": "test"}}]}`,
+			wantErr:   true,
+		},
+		{
+			name:   "Record Invalid Field Type",
+			schema: `{"type":"record", "name":"test", "namespace": "org.apache.avro", "fields":[{"name": "a", "type": {"type":"record", "name": "test2", "fields":[{"name": "b", "type": "int"},{"name": "c", "type": "int", "default": "test"}]}, "default": {"b": 1}}]}`,
+			wantErr:   true,
 		},
 	}
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			s, err := avro.Parse(tt.schema)
+			_, err := avro.Parse(tt.schema)
+
+			if tt.wantErr {
+				assert.Error(t, err)
+				return
+			}
 
 			assert.NoError(t, err)
-			assert.Equal(t, tt.want, s.(*avro.RecordSchema).Fields()[0].Default())
 		})
 	}
 }
@@ -386,7 +455,7 @@ func TestEnumSchema(t *testing.T) {
 			assert.NoError(t, err)
 			assert.Equal(t, avro.Enum, schema.Type())
 			named := schema.(avro.NamedSchema)
-			assert.Equal(t, tt.wantName, named.Name())
+			assert.Equal(t, tt.wantName, named.FullName())
 		})
 	}
 }
@@ -486,6 +555,21 @@ func TestUnionSchema(t *testing.T) {
 			wantFingerprint: [32]byte{0xb4, 0x94, 0x95, 0xc5, 0xb1, 0xc2, 0x6f, 0x4, 0x89, 0x6a, 0x5f, 0x68, 0x65, 0xf, 0xe2, 0xb7, 0x64, 0x23, 0x62, 0xc3, 0x41, 0x98, 0xd6, 0xbc, 0x74, 0x65, 0xa1, 0xd9, 0xf7, 0xe1, 0xaf, 0xce},
 			wantErr:         false,
 		},
+		{
+			name:    "No Nested Union Type",
+			schema:  `["null", ["string"]]`,
+			wantErr: true,
+		},
+		{
+			name:    "No Duplicate Types",
+			schema:  `["string", "string"]`,
+			wantErr: true,
+		},
+		{
+			name:    "No Duplicate Names",
+			schema:  `[{"type":"enum", "name":"test", "symbols":["TEST"]}, {"type":"enum", "name":"test", "symbols":["TEST"]}]`,
+			wantErr: true,
+		},
 		{
 			name:    "Invalid Type",
 			schema:  `["null", "blah"]`,
@@ -573,7 +657,7 @@ func TestFixedSchema(t *testing.T) {
 			assert.NoError(t, err)
 			assert.Equal(t, avro.Fixed, schema.Type())
 			named := schema.(avro.NamedSchema)
-			assert.Equal(t, tt.wantName, named.Name())
+			assert.Equal(t, tt.wantName, named.FullName())
 			assert.Equal(t, tt.wantFingerprint, named.Fingerprint())
 		})
 	}