diff --git a/schema.go b/schema.go index 7e904c9..623c226 100644 --- a/schema.go +++ b/schema.go @@ -1429,9 +1429,22 @@ func (s *FixedSchema) CacheFingerprint() [32]byte { // NullSchema is an Avro null type schema. type NullSchema struct { + properties fingerprinter } +// NewNullSchema creates a new NullSchema. +func NewNullSchema(opts ...SchemaOption) *NullSchema { + var cfg schemaConfig + for _, opt := range opts { + opt(&cfg) + } + + return &NullSchema{ + properties: newProperties(cfg.props, primitiveReserved), + } +} + // Type returns the type of the schema. func (s *NullSchema) Type() Type { return Null @@ -1444,7 +1457,16 @@ func (s *NullSchema) String() string { // MarshalJSON marshals the schema to json. func (s *NullSchema) MarshalJSON() ([]byte, error) { - return []byte(`"null"`), nil + if len(s.props) == 0 { + return []byte(`"null"`), nil + } + buf := new(bytes.Buffer) + buf.WriteString(`{"type":"null"`) + if err := s.marshalPropertiesToJSON(buf); err != nil { + return nil, err + } + buf.WriteString("}") + return buf.Bytes(), nil } // Fingerprint returns the SHA256 fingerprint of the schema. diff --git a/schema_json_test.go b/schema_json_test.go index 2db4ac0..40f04ad 100644 --- a/schema_json_test.go +++ b/schema_json_test.go @@ -23,6 +23,10 @@ func TestSchema_JSON(t *testing.T) { input: `{"type":"null"}`, json: `"null"`, }, + { + input: `{"type":"null","other":"foo"}`, + json: `{"type":"null","other":"foo"}`, + }, { input: `"boolean"`, json: `"boolean"`, diff --git a/schema_parse.go b/schema_parse.go index 71af2d3..706250b 100644 --- a/schema_parse.go +++ b/schema_parse.go @@ -135,13 +135,7 @@ func parseComplexType(namespace string, m map[string]any, seen seenCache, cache typ := Type(str) switch typ { - case Null: - // TODO: Per the spec, "null" is a primitive type so should be permitted to - // have other properties/metadata. - // https://avro.apache.org/docs/1.12.0/specification/#primitive-types - return &NullSchema{}, nil - - case String, Bytes, Int, Long, Float, Double, Boolean: + case String, Bytes, Int, Long, Float, Double, Boolean, Null: return parsePrimitive(typ, m) case Record, Error: @@ -172,6 +166,9 @@ type primitiveSchema struct { func parsePrimitive(typ Type, m map[string]any) (Schema, error) { if len(m) == 0 { + if typ == Null { + return &NullSchema{}, nil + } return NewPrimitiveSchema(typ, nil), nil } @@ -191,6 +188,9 @@ func parsePrimitive(typ Type, m map[string]any) (Schema, error) { } } + if typ == Null { + return NewNullSchema(WithProps(p.Props)), nil + } return NewPrimitiveSchema(typ, logical, WithProps(p.Props)), nil } diff --git a/schema_test.go b/schema_test.go index 3e10cb8..4d0c064 100644 --- a/schema_test.go +++ b/schema_test.go @@ -58,6 +58,7 @@ func TestNullSchema(t *testing.T) { schemas := []string{ `null`, `{"type":"null"}`, + `{"type":"null", "other-property": 123, "another-property": ["a","b","c"]}`, } for _, schm := range schemas { @@ -1892,6 +1893,27 @@ func TestParse_PreservesAllProperties(t *testing.T) { }, rec.Props()) }, }, + { + name: "null", + schema: `{ + "type": "null", + "name": "SomeMap", + "logicalType": "weights", + "precision": "abc", + "scale": "def", + "other": [1,2,3] + }`, + check: func(t *testing.T, schema avro.Schema) { + rec := schema.(*avro.NullSchema) + assert.Equal(t, map[string]any{ + "name": "SomeMap", + "logicalType": "weights", + "precision": "abc", + "scale": "def", + "other": []any{1.0, 2.0, 3.0}, + }, rec.Props()) + }, + }, } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { @@ -2141,4 +2163,30 @@ func TestNewSchema_IgnoresInvalidProperties(t *testing.T) { "other": true, }, rec.Props()) }) + t.Run("null", func(t *testing.T) { + rec := avro.NewNullSchema( + avro.WithProps(map[string]any{ + // invalid (conflict with other type properties) + "type": false, + // valid + "name": 123, + "namespace": "abc", + "doc": "blah", + "aliases": "foo", + "other": true, + "logicalType": "baz", + "precision": "abc", + "scale": "def", + })) + assert.Equal(t, map[string]any{ + "name": 123, + "namespace": "abc", + "doc": "blah", + "aliases": "foo", + "other": true, + "logicalType": "baz", + "precision": "abc", + "scale": "def", + }, rec.Props()) + }) }