Repository: hamba/avro
Pull request #278 (2023-08-01 02:14:24)

feat: add properties to JSON output
Helps #275


Patch:
diff --git a/schema.go b/schema.go
index 9e2168d..b7438a2 100644
--- a/schema.go
+++ b/schema.go
@@ -1,11 +1,13 @@
 package avro
 
 import (
+	"bytes"
 	"crypto/md5"
 	"crypto/sha256"
 	"errors"
 	"fmt"
 	"hash"
+	"sort"
 	"strconv"
 	"strings"
 	"sync"
@@ -314,6 +316,23 @@ func (p properties) Prop(name string) any {
 	return p.props[name]
 }
 
+func (p properties) marshalPropertiesToJSON(buf *bytes.Buffer) error {
+	sortedPropertyKeys := make([]string, 0, len(p.props))
+	for k := range p.props {
+		sortedPropertyKeys = append(sortedPropertyKeys, k)
+	}
+	sort.Strings(sortedPropertyKeys)
+	for _, k := range sortedPropertyKeys {
+		vv, err := jsoniter.Marshal(p.props[k])
+		if err != nil {
+			return err
+		}
+		buf.WriteString(`,"` + k + `":`)
+		buf.Write(vv)
+	}
+	return nil
+}
+
 type schemaConfig struct {
 	aliases []string
 	doc     string
@@ -404,7 +423,26 @@ func (s *PrimitiveSchema) String() string {
 
 // MarshalJSON marshals the schema to json.
 func (s *PrimitiveSchema) MarshalJSON() ([]byte, error) {
-	return []byte(s.String()), nil
+	if s.logical == nil && len(s.props) == 0 {
+		return jsoniter.Marshal(s.typ)
+	}
+
+	buf := new(bytes.Buffer)
+	buf.WriteString(`{"type":"` + string(s.typ) + `"`)
+	if s.logical != nil {
+		buf.WriteString(`,"logicalType":"` + string(s.logical.Type()) + `"`)
+		if d, ok := s.logical.(*DecimalLogicalSchema); ok {
+			buf.WriteString(`,"precision":` + strconv.Itoa(d.prec))
+			if d.scale > 0 {
+				buf.WriteString(`,"scale":` + strconv.Itoa(d.scale))
+			}
+		}
+	}
+	if err := s.marshalPropertiesToJSON(buf); err != nil {
+		return nil, err
+	}
+	buf.WriteString("}")
+	return buf.Bytes(), nil
 }
 
 // Fingerprint returns the SHA256 fingerprint of the schema.
@@ -500,26 +538,35 @@ func (s *RecordSchema) String() string {
 
 // MarshalJSON marshals the schema to json.
 func (s *RecordSchema) MarshalJSON() ([]byte, error) {
-	typ := "record"
+	buf := new(bytes.Buffer)
+	buf.WriteString(`{"name":"` + s.full + `"`)
+	if len(s.aliases) > 0 {
+		aliasesJSON, err := jsoniter.Marshal(s.aliases)
+		if err != nil {
+			return nil, err
+		}
+		buf.WriteString(`,"aliases":`)
+		buf.Write(aliasesJSON)
+	}
+	if s.doc != "" {
+		buf.WriteString(`,"doc":"` + s.doc + `"`)
+	}
 	if s.isError {
-		typ = "error"
+		buf.WriteString(`,"type":"error"`)
+	} else {
+		buf.WriteString(`,"type":"record"`)
 	}
-
-	ss := struct {
-		Name    string   `json:"name"`
-		Aliases []string `json:"aliases,omitempty"`
-		Doc     string   `json:"doc,omitempty"`
-		Type    string   `json:"type"`
-		Fields  []*Field `json:"fields"`
-	}{
-		Name:    s.full,
-		Aliases: s.aliases,
-		Doc:     s.doc,
-		Type:    typ,
-		Fields:  s.fields,
+	fieldsJSON, err := jsoniter.Marshal(s.fields)
+	if err != nil {
+		return nil, err
 	}
-
-	return jsoniter.Marshal(ss)
+	buf.WriteString(`,"fields":`)
+	buf.Write(fieldsJSON)
+	if err := s.marshalPropertiesToJSON(buf); err != nil {
+		return nil, err
+	}
+	buf.WriteString("}")
+	return buf.Bytes(), nil
 }
 
 // Fingerprint returns the SHA256 fingerprint of the schema.
@@ -643,36 +690,41 @@ func (f *Field) String() string {
 
 // MarshalJSON marshals the schema to json.
 func (f *Field) MarshalJSON() ([]byte, error) {
-	type base struct {
-		Name    string   `json:"name"`
-		Aliases []string `json:"aliases,omitempty"`
-		Doc     string   `json:"doc,omitempty"`
-		Type    Schema   `json:"type"`
-		Order   string   `json:"order,omitempty"`
-	}
-	type ext struct {
-		base
-		Default any `json:"default"`
+	buf := new(bytes.Buffer)
+	buf.WriteString(`{"name":"` + f.name + `"`)
+	if len(f.aliases) > 0 {
+		aliasesJSON, err := jsoniter.Marshal(f.aliases)
+		if err != nil {
+			return nil, err
+		}
+		buf.WriteString(`,"aliases":`)
+		buf.Write(aliasesJSON)
 	}
-
-	b := base{
-		Name:    f.name,
-		Aliases: f.aliases,
-		Doc:     f.doc,
-		Type:    f.typ,
+	if f.doc != "" {
+		buf.WriteString(`,"doc":"` + f.doc + `"`)
 	}
-	if f.order != Asc {
-		b.Order = string(f.order)
+	typeJSON, err := jsoniter.Marshal(f.typ)
+	if err != nil {
+		return nil, err
 	}
-
-	var s any = b
+	buf.WriteString(`,"type":`)
+	buf.Write(typeJSON)
 	if f.hasDef {
-		s = ext{
-			base:    s.(base),
-			Default: f.Default(),
+		defaultValueJSON, err := jsoniter.Marshal(f.Default())
+		if err != nil {
+			return nil, err
 		}
+		buf.WriteString(`,"default":`)
+		buf.Write(defaultValueJSON)
+	}
+	if f.order != "" && f.order != Asc {
+		buf.WriteString(`,"order":"` + string(f.order) + `"`)
 	}
-	return jsoniter.Marshal(s)
+	if err := f.marshalPropertiesToJSON(buf); err != nil {
+		return nil, err
+	}
+	buf.WriteString("}")
+	return buf.Bytes(), nil
 }
 
 // EnumSchema is an Avro enum type schema.
@@ -769,22 +821,34 @@ func (s *EnumSchema) String() string {
 
 // MarshalJSON marshals the schema to json.
 func (s *EnumSchema) MarshalJSON() ([]byte, error) {
-	ss := struct {
-		Name    string   `json:"name"`
-		Aliases []string `json:"aliases,omitempty"`
-		Doc     string   `json:"doc,omitempty"`
-		Type    string   `json:"type"`
-		Symbols []string `json:"symbols"`
-		Default string   `json:"default,omitempty"`
-	}{
-		Name:    s.full,
-		Aliases: s.aliases,
-		Doc:     s.doc,
-		Type:    "enum",
-		Symbols: s.symbols,
-		Default: s.def,
-	}
-	return jsoniter.Marshal(ss)
+	buf := new(bytes.Buffer)
+	buf.WriteString(`{"name":"` + s.full + `"`)
+	if len(s.aliases) > 0 {
+		aliasesJSON, err := jsoniter.Marshal(s.aliases)
+		if err != nil {
+			return nil, err
+		}
+		buf.WriteString(`,"aliases":`)
+		buf.Write(aliasesJSON)
+	}
+	if s.doc != "" {
+		buf.WriteString(`,"doc":"` + s.doc + `"`)
+	}
+	buf.WriteString(`,"type":"enum"`)
+	symbolsJSON, err := jsoniter.Marshal(s.symbols)
+	if err != nil {
+		return nil, err
+	}
+	buf.WriteString(`,"symbols":`)
+	buf.Write(symbolsJSON)
+	if s.def != "" {
+		buf.WriteString(`,"default":"` + s.def + `"`)
+	}
+	if err := s.marshalPropertiesToJSON(buf); err != nil {
+		return nil, err
+	}
+	buf.WriteString("}")
+	return buf.Bytes(), nil
 }
 
 // Fingerprint returns the SHA256 fingerprint of the schema.
@@ -835,14 +899,19 @@ func (s *ArraySchema) String() string {
 
 // MarshalJSON marshals the schema to json.
 func (s *ArraySchema) MarshalJSON() ([]byte, error) {
-	ss := struct {
-		Type  string `json:"type"`
-		Items Schema `json:"items"`
-	}{
-		Type:  "array",
-		Items: s.items,
+	buf := new(bytes.Buffer)
+	buf.WriteString(`{"type":"array"`)
+	itemsJSON, err := jsoniter.Marshal(s.items)
+	if err != nil {
+		return nil, err
+	}
+	buf.WriteString(`,"items":`)
+	buf.Write(itemsJSON)
+	if err = s.marshalPropertiesToJSON(buf); err != nil {
+		return nil, err
 	}
-	return jsoniter.Marshal(ss)
+	buf.WriteString("}")
+	return buf.Bytes(), nil
 }
 
 // Fingerprint returns the SHA256 fingerprint of the schema.
@@ -893,14 +962,19 @@ func (s *MapSchema) String() string {
 
 // MarshalJSON marshals the schema to json.
 func (s *MapSchema) MarshalJSON() ([]byte, error) {
-	ss := struct {
-		Type   string `json:"type"`
-		Values Schema `json:"values"`
-	}{
-		Type:   "map",
-		Values: s.values,
+	buf := new(bytes.Buffer)
+	buf.WriteString(`{"type":"map"`)
+	valuesJSON, err := jsoniter.Marshal(s.values)
+	if err != nil {
+		return nil, err
+	}
+	buf.WriteString(`,"values":`)
+	buf.Write(valuesJSON)
+	if err := s.marshalPropertiesToJSON(buf); err != nil {
+		return nil, err
 	}
-	return jsoniter.Marshal(ss)
+	buf.WriteString("}")
+	return buf.Bytes(), nil
 }
 
 // Fingerprint returns the SHA256 fingerprint of the schema.
@@ -1065,28 +1139,32 @@ func (s *FixedSchema) String() string {
 
 // MarshalJSON marshals the schema to json.
 func (s *FixedSchema) MarshalJSON() ([]byte, error) {
-	ss := struct {
-		Name    string   `json:"name"`
-		Aliases []string `json:"aliases,omitempty"`
-		Type    string   `json:"type"`
-		Size    int      `json:"size"`
-	}{
-		Name:    s.full,
-		Aliases: s.aliases,
-		Type:    "fixed",
-		Size:    s.size,
-	}
-	json, err := jsoniter.MarshalToString(ss)
-	if err != nil {
-		return nil, err
+	buf := new(bytes.Buffer)
+	buf.WriteString(`{"name":"` + s.full + `"`)
+	if len(s.aliases) > 0 {
+		aliasesJSON, err := jsoniter.Marshal(s.aliases)
+		if err != nil {
+			return nil, err
+		}
+		buf.WriteString(`,"aliases":`)
+		buf.Write(aliasesJSON)
 	}
-
-	var logical string
+	buf.WriteString(`,"type":"fixed"`)
+	buf.WriteString(`,"size":` + strconv.Itoa(s.size))
 	if s.logical != nil {
-		logical = "," + s.logical.String()
+		buf.WriteString(`,"logicalType":"` + string(s.logical.Type()) + `"`)
+		if d, ok := s.logical.(*DecimalLogicalSchema); ok {
+			buf.WriteString(`,"precision":` + strconv.Itoa(d.prec))
+			if d.scale > 0 {
+				buf.WriteString(`,"scale":` + strconv.Itoa(d.scale))
+			}
+		}
 	}
-
-	return []byte(json[:len(json)-1] + logical + "}"), nil
+	if err := s.marshalPropertiesToJSON(buf); err != nil {
+		return nil, err
+	}
+	buf.WriteString("}")
+	return buf.Bytes(), nil
 }
 
 // Fingerprint returns the SHA256 fingerprint of the schema.


Test patch:
diff --git a/schema_json_test.go b/schema_json_test.go
index 7fb30f1..0021e51 100644
--- a/schema_json_test.go
+++ b/schema_json_test.go
@@ -59,6 +59,10 @@ func TestSchema_JSON(t *testing.T) {
 			input: `{"type":"long"}`,
 			json:  `"long"`,
 		},
+		{
+			input: `{"property-b": "value-bar", "type":"long", "property-a": "value-foo"}`,
+			json:  `{"type":"long","property-a":"value-foo","property-b":"value-bar"}`,
+		},
 		{
 			input: `{"type":"long","logicalType":"time-micros"}`,
 			json:  `{"type":"long","logicalType":"time-micros"}`,
@@ -155,6 +159,10 @@ func TestSchema_JSON(t *testing.T) {
 			input: `{"fields":[], "type":"record", "name":"foo", "doc":"foo", "aliases":["foo","bar"]}`,
 			json:  `{"name":"foo","aliases":["foo","bar"],"doc":"foo","type":"record","fields":[]}`,
 		},
+		{
+			input: `{"fields":[], "property-foo": "value-bar", "type":"record", "name":"foo", "doc":"foo", "aliases":["foo","bar"]}`,
+			json:  `{"name":"foo","aliases":["foo","bar"],"doc":"foo","type":"record","fields":[],"property-foo":"value-bar"}`,
+		},
 		{
 			input: `{"fields":[{"type":{"type":"boolean"}, "name":"f1"}], "type":"record", "name":"foo"}`,
 			json:  `{"name":"foo","type":"record","fields":[{"name":"f1","type":"boolean"}]}`,