Documentation
¶
Overview ¶
Package avro encodes and decodes Avro specification data.
Parse an Avro JSON schema with Parse (or MustParse for package-level vars), then call Schema.Encode / Schema.Decode for binary encoding, or Schema.EncodeJSON / Schema.DecodeJSON for JSON encoding. Use SchemaFor to infer a schema from a Go struct type, or Schema.Root to inspect a parsed schema's structure.
Basic usage ¶
schema := avro.MustParse(`{
"type": "record",
"name": "User",
"fields": [
{"name": "name", "type": "string"},
{"name": "age", "type": "int"}
]
}`)
type User struct {
Name string `avro:"name"`
Age int `avro:"age"`
}
// Encode
data, err := schema.Encode(&User{Name: "Alice", Age: 30})
// Decode
var u User
_, err = schema.Decode(data, &u)
JSON encoding ¶
Schema.EncodeJSON is schema-aware: we handle bytes, unions, and NaN/Infinity floats correctly. Use it rather than a generic JSON encoder when serializing decoded Avro data. Options control the output format: TaggedUnions for Avro JSON union wrappers ({"type": value}), TagLogicalTypes for qualified branch names, and LinkedinFloats for the goavro NaN/Infinity convention.
Encoding from JSON input ¶
You can encode generically-decoded JSON data (map[string]any with float64 numbers and string timestamps) directly. We fill missing map keys from schema defaults. encoding/json.Number is accepted for numeric Avro types only: string, bytes, fixed, and enum reject it, so use a Go string or []byte for those. Timestamp fields accept RFC 3339 strings, and string fields accept encoding.TextAppender and encoding.TextMarshaler implementations (with encoding.TextUnmarshaler on decode).
Schema evolution ¶
Avro data is always written with a specific schema, the "writer schema". When you read it later, your application may expect a different "reader schema": you may have added a field, removed one, or widened an int to a long.
Resolve takes the writer and reader schemas and returns a schema that decodes data in the writer's wire format into the reader's layout:
- We fill reader-only fields from defaults.
- We skip writer-only fields.
- We match fields in both by name (or alias) and decode them, promoting types where needed (e.g. int to long).
You usually get the writer schema from the data itself: an OCF file header embeds it, and schema registries store it by ID or fingerprint.
As a concrete example, suppose v1 of your application wrote User records with just a name:
var writerSchema = avro.MustParse(`{
"type": "record", "name": "User",
"fields": [
{"name": "name", "type": "string"}
]
}`)
In v2, you added an email field with a default:
var readerSchema = avro.MustParse(`{
"type": "record", "name": "User",
"fields": [
{"name": "name", "type": "string"},
{"name": "email", "type": "string", "default": ""}
]
}`)
type User struct {
Name string `avro:"name"`
Email string `avro:"email"`
}
To read old v1 data with your v2 struct, resolve the two schemas:
resolved, err := avro.Resolve(writerSchema, readerSchema)
// Decode v1 data: "email" is absent in the old data, so it gets
// the reader default ("").
var u User
_, err = resolved.Decode(v1Data, &u)
// u == User{Name: "Alice", Email: ""}
If you only want to check whether two schemas are compatible, use CheckCompatibility.
A null union branch decodes to the target's Go zero value, replacing any prior value. Use *T if you need to tell null from zero.
Precision follows the reader schema. A float or double schema rounds silently on both encode and decode, and a finite value out of range becomes ±Inf on the wire. An int, long, bytes, or string schema never loses precision silently: decoding into a Go type that cannot hold the value exactly, such as a long above 2^53 into a float64, is an error. If you need large integers to round-trip exactly, keep the reader schema long and decode into an int64.
Struct tags ¶
Use the "avro" struct tag to control field mapping and schema inference. The format is avro:"[name][,option]..." where the name maps the Go field to the Avro field name (empty = use Go field name, "-" = exclude).
Encoding/decoding options:
avro:"name" // map to Avro field "name" avro:"-" // exclude field avro:",inline" // flatten nested struct fields into parent record avro:",omitzero" // encode a zero value as the field's default (or null)
Schema inference options (used by SchemaFor):
avro:",default=value" // field default; last option, scalars only avro:",alias=old_name" // field alias; repeatable, or alias=[a,b] avro:",type-alias=old_name" // named type alias; same two spellings avro:",timestamp-micros" // logical type; see the list below avro:",decimal(10,2)" // decimal logical type, precision and scale avro:",uuid" // UUID logical type
The logical types you can override with are timestamp-millis, timestamp-micros, timestamp-nanos, date, time-millis, and time-micros.
The alias tag adds an alias to the field itself. The type-alias tag adds an alias to the named type (record, enum, or fixed) that the field references; we walk through pointers, slices, and maps to find it. You need it when a writer schema uses a different name for the same type: a legacy schema naming a record "r508" instead of "FieldSummary".
When you encode a map[string]any as a record, we fill missing keys from the schema's defaults. A ["null", T] field with no default has an implicit null default, so a missing key there fills null rather than erroring. The omitzero tag applies the same fill to a struct's zero-valued fields, and to fields whose IsZero() method reports true: a zero value encodes the field's default, or null for a nullable field with no default. A non-nullable field with no default has nothing to fill with, so it encodes the zero value itself. Note that omitzero differs from map fill for a [T, "null"] union with no default: a union default must match the first branch, so no null default can exist there, and omitzero encodes null where map fill errors on the missing key.
We inline embedded (anonymous) struct fields automatically; an explicit name tag prevents it. When several fields resolve to one name, a tagged field wins over an untagged one at any depth, and among equally tagged fields the shallowest wins. Two fields at the same depth with the same tagged status are ambiguous, and we error rather than pick one. SchemaFor rejects the type outright, while encode and decode reject only when the schema actually has a field with that name.
Custom types ¶
CustomType registers custom Go type conversions for logical types, domain types, or to replace our built-in behavior. A matching custom type replaces our built-in logical type deserializer: your Decode callback receives raw Avro-native values, not enriched types like time.Time. A CustomType with nil Decode suppresses the built-in handler with zero overhead, producing raw values directly. Use NewCustomType for type-safe primitive conversions, or the CustomType struct directly for complex cases (records, fixed types, property-based dispatch). You register custom types per-schema via SchemaOpt.
Parsing options ¶
Parse and SchemaCache.Parse accept WithLaxNames to allow non-standard characters in type and field names.
Errors ¶
You can inspect encode and decode errors with errors.As:
- *SemanticError: type mismatch, with a dotted field path for nested records
- *ShortBufferError: input truncated mid-value
- *CompatibilityError: schema evolution incompatibility
Other features ¶
- Schema Cache: SchemaCache accumulates named types across Parse calls, for schema registry workflows
- Schema Introspection: Schema.Root returns a SchemaNode; Schema.String returns the original JSON
- Single Object Encoding: Schema.AppendSingleObject, Schema.DecodeSingleObject
- Fingerprinting: Schema.Canonical, Schema.Fingerprint, NewRabin
- Object Container Files: the github.com/twmb/avro/ocf sub-package
The README's "Encode/decode behavior contract" section lists the intentional asymmetries between the encoder and the decoder.
Example ¶
package main
import (
"fmt"
"log"
"github.com/twmb/avro"
)
func main() {
schema := avro.MustParse(`{
"type": "record",
"name": "User",
"fields": [
{"name": "name", "type": "string"},
{"name": "age", "type": "int"}
]
}`)
type User struct {
Name string `avro:"name"`
Age int32 `avro:"age"`
}
data, err := schema.Encode(&User{Name: "Alice", Age: 30})
if err != nil {
log.Fatal(err)
}
var u User
if _, err := schema.Decode(data, &u); err != nil {
log.Fatal(err)
}
fmt.Printf("%s is %d\n", u.Name, u.Age)
}
Output: Alice is 30
Index ¶
- Variables
- func CheckCompatibility(writer, reader *Schema) error
- func NewRabin() hash.Hash64
- func RatFromBytes(b []byte, scale int) *big.Rat
- func SingleObjectFingerprint(data []byte) (fp [8]byte, rest []byte, err error)
- type CompatibilityError
- type CustomType
- type Duration
- type Opt
- type Schema
- func (s *Schema) AppendEncode(dst []byte, v any, opts ...Opt) ([]byte, error)
- func (s *Schema) AppendEncodeJSON(dst []byte, v any, opts ...Opt) ([]byte, error)
- func (s *Schema) AppendSingleObject(dst []byte, v any, opts ...Opt) ([]byte, error)
- func (s *Schema) Canonical() []byte
- func (s *Schema) Decode(src []byte, v any, opts ...Opt) ([]byte, error)
- func (s *Schema) DecodeJSON(src []byte, v any, opts ...Opt) error
- func (s *Schema) DecodeSingleObject(data []byte, v any, opts ...Opt) ([]byte, error)
- func (s *Schema) Encode(v any, opts ...Opt) ([]byte, error)
- func (s *Schema) EncodeJSON(v any, opts ...Opt) ([]byte, error)
- func (s *Schema) Fingerprint(h hash.Hash) []byte
- func (s *Schema) Root() *SchemaNode
- func (s *Schema) String() string
- type SchemaCache
- type SchemaField
- type SchemaNode
- type SchemaOpt
- type SemanticError
- type ShortBufferError
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrSkipCustomType = errors.New("avro: skip custom type")
ErrSkipCustomType is returned from a CustomType Encode or Decode function to say this custom type does not handle the value. We fall through to the next matching custom type, or to built-in behavior.
Functions ¶
func CheckCompatibility ¶
CheckCompatibility reports whether data written with the writer schema can be read by the reader schema. We return nil on success, or a *CompatibilityError describing the first incompatibility.
See Resolve for a note on argument order.
func NewRabin ¶
NewRabin returns a hash.Hash64 computing the CRC-64-AVRO (Rabin) fingerprint defined by the Avro specification.
func RatFromBytes ¶ added in v1.5.0
RatFromBytes converts Avro decimal bytes (big-endian two's complement) to a *big.Rat with the given scale. This is the conversion you would otherwise write yourself in a CustomType Decode callback that overrides our built-in decimal handling, since such a callback receives the raw []byte.
We read a negative scale as unscaled * 10^|scale|, matching Java and avro-rs. We bound |scale| and the unscaled byte length; input past either bound returns a zero *big.Rat rather than allocating unbounded.
Types ¶
type CompatibilityError ¶
type CompatibilityError struct {
// Path is the dotted path to the incompatible element, e.g.
// "User.address.zip".
Path string
// ReaderType is the Avro type in the reader schema.
ReaderType string
// WriterType is the Avro type in the writer schema.
WriterType string
// Detail describes the specific incompatibility.
Detail string
}
CompatibilityError describes an incompatibility between a reader and writer schema, as returned by CheckCompatibility and Resolve.
func (*CompatibilityError) Error ¶
func (e *CompatibilityError) Error() string
type CustomType ¶ added in v1.3.0
type CustomType struct {
// LogicalType narrows matching to schema nodes with this logicalType.
LogicalType string
// AvroType narrows matching to schema nodes of this Avro type
// (e.g. "long", "bytes", "record"). Also used by SchemaFor to
// infer the underlying Avro type.
AvroType string
// GoType adds an encode-time filter: when set, your Encode function
// only fires when the value's concrete type matches GoType. Values
// of other types pass through to the underlying serializer unchanged.
// If nil, Encode fires for all values on matched schema nodes
// (those matching LogicalType/AvroType).
//
// [SchemaFor] uses GoType to match struct fields: when a field's Go
// type equals GoType, we emit AvroType + LogicalType (or Schema)
// instead of the default type mapping. Because the custom supplies
// the whole field schema, a logical-type tag on a matched field has
// no effect and is rejected: set LogicalType (or Schema) here
// instead. If nil, the custom type does not affect schema
// generation, but we still wire it into the returned [*Schema] for
// encode/decode.
GoType reflect.Type
// Schema is the full schema to emit in SchemaFor. You only need it
// for types requiring extra metadata (fixed needs name+size, decimal
// needs precision+scale, records need fields). If nil, we infer from
// AvroType + LogicalType.
//
// We preserve every fullname the schema declares: a namespaced type
// keeps its namespace, and a null-namespace type embedded under
// [WithNamespace] keeps its null namespace (the emitted definition
// carries a "namespace":"" escape). Note that a null-namespace type
// used on two or more fields under WithNamespace is an error, because
// Avro has no way to reference the null namespace from inside another
// namespace.
//
// We work on a private copy, so SchemaFor never mutates your
// SchemaNode or anything reachable from it. A schema over the
// schema-tree budgets, or one holding an unnamed pointer cycle, fails
// the build with an error.
//
// A union branch may be written either as a bare name ("null") or as
// a wrapped object ({"type":"null"}); we treat the two as the same
// type. A null branch is recognized in both spellings, whatever
// properties or logicalType the wrapped form carries (Avro defines no
// null logical type), so a nullable union collapses through a pointer
// field and receives its null default the same way either way.
Schema *SchemaNode
// Encode converts your Go value to an Avro-native value. We call it
// before serialization with the value as you passed it to
// [Schema.Encode] (e.g. a custom Money type); return the
// corresponding Avro-native value (e.g. int64 cents). Return
// [ErrSkipCustomType] to fall through to the next matching custom
// type or built-in behavior. Any other non-nil error is fatal.
//
// If nil, we use the built-in logical type encoder, which accepts
// both enriched types ([time.Time], [time.Duration]) and raw
// values (int64, int32, etc.).
//
// We build the schema argument once at Parse and share it across all
// concurrent invocations. Treat it as read-only; in particular, do
// not mutate schema.Props or schema.Symbols, whose slices and maps
// alias the parser's internal state: concurrent writes from multiple
// goroutines decoding the same [*Schema] will race.
Encode func(v any, schema *SchemaNode) (any, error)
// Decode converts a raw Avro-native value to your Go value. We call
// it after deserialization with the raw Avro-native value (int32 for
// int, int64 for long, []byte for bytes/fixed, etc.); return the
// type you want. Return [ErrSkipCustomType] to fall through. Any
// other non-nil error is fatal.
//
// When all matching decoders skip at a node, we re-decode the wire
// into the target faithfully (identical to a no-custom decode). A
// wildcard custom (empty LogicalType and AvroType) that matches leaf
// nodes but skips containers therefore makes decoding into a
// deeply-nested *typed* target (struct/slice/map) cost O(depth^2).
// For untrusted deeply-nested data, decode into an interface /
// map[string]any (single-pass) or register against a specific
// LogicalType/AvroType.
//
// If nil, we bypass the built-in logical type handler and use the
// base Avro type decoder directly, producing raw Avro-native values
// (int32, int64, etc.) rather than enriched types ([time.Time],
// [time.Duration], etc.).
//
// The schema argument is shared across concurrent callback invocations;
// see [CustomType.Encode] for the read-only contract.
//
// Under [AliasInput], a []byte v points into the decode input, and a
// field filled from its schema default points into the parsed
// [Schema], which every decode of that schema shares. Read it or copy
// from it, but do not write through it. Returning it is fine.
Decode func(v any, schema *SchemaNode) (any, error)
// contains filtered or unexported fields
}
CustomType defines a custom conversion between a Go type and an Avro type. NewCustomType is the simpler form for types backed by a primitive Avro type; this struct is the general form, and the only way to handle records, fixed types, and property-based dispatch.
Pass a CustomType to Parse or SchemaFor as a SchemaOpt.
At parse time we match LogicalType and AvroType against schema nodes. All non-empty criteria must match:
- LogicalType only: matches any schema node with that logicalType
- LogicalType + AvroType: matches that logicalType on that Avro type
- AvroType only: matches all nodes of that Avro type
- Neither: matches every schema node (use with ErrSkipCustomType for property-based dispatch like Kafka Connect types)
At encode time we check GoType too: your Encode function only fires when the value's type matches GoType. This keeps the codec from intercepting native values (e.g. a raw int64 passes through without conversion for a custom-typed long field).
A matching CustomType replaces our built-in logical type deserializer. Among your registrations, the first match wins.
If the Avro type is complex, your Encode function returns map[string]any, []any, and so on.
Example (Override) ¶
package main
import (
"fmt"
"github.com/twmb/avro"
)
func main() {
// Use CustomType directly to override a built-in logical type handler.
// Here we suppress the timestamp-millis to time.Time conversion and
// keep the raw int64 epoch millis.
schema := avro.MustParse(`{
"type": "record", "name": "Event",
"fields": [
{"name": "ts", "type": {"type": "long", "logicalType": "timestamp-millis"}}
]
}`, avro.CustomType{
LogicalType: "timestamp-millis",
Decode: func(v any, _ *avro.SchemaNode) (any, error) {
return v, nil // pass through raw int64
},
})
data, _ := schema.Encode(map[string]any{"ts": int64(1767225600000)})
var out any
schema.Decode(data, &out)
m := out.(map[string]any)
fmt.Printf("ts type: %T\n", m["ts"])
}
Output: ts type: int64
Example (PropertyDispatch) ¶
package main
import (
"fmt"
"github.com/twmb/avro"
)
func main() {
// CustomType with no LogicalType/AvroType/GoType matches every schema
// node. Use ErrSkipCustomType to selectively handle nodes based on
// schema properties, e.g. Kafka Connect type annotations.
ct := avro.CustomType{
Decode: func(v any, node *avro.SchemaNode) (any, error) {
if node.Props["connect.type"] == "double-it" {
return v.(int64) * 2, nil
}
return nil, avro.ErrSkipCustomType
},
}
// Properties on the type object are available via node.Props in the
// custom type callback.
schema := avro.MustParse(`{
"type": "record", "name": "R",
"fields": [
{"name": "x", "type": {"type": "long", "connect.type": "double-it"}},
{"name": "y", "type": "long"}
]
}`, ct)
data, _ := schema.Encode(map[string]any{"x": int64(5), "y": int64(5)})
var out any
schema.Decode(data, &out)
m := out.(map[string]any)
fmt.Printf("x=%d y=%d\n", m["x"], m["y"])
}
Output: x=10 y=5
Example (SchemaFor) ¶
package main
import (
"fmt"
"log"
"reflect"
"github.com/twmb/avro"
)
func main() {
// Setting GoType lets SchemaFor infer the Avro schema for struct
// fields of that type. Without GoType, SchemaFor doesn't know that
// a Cents field should map to {"type":"long","logicalType":"money"}.
type Cents int64
ct := avro.CustomType{
LogicalType: "money",
AvroType: "long",
GoType: reflect.TypeFor[Cents](),
Encode: func(v any, _ *avro.SchemaNode) (any, error) {
return int64(v.(Cents)), nil
},
Decode: func(v any, _ *avro.SchemaNode) (any, error) {
return Cents(v.(int64)), nil
},
}
type Order struct {
Price Cents `avro:"price"`
}
schema, err := avro.SchemaFor[Order](ct)
if err != nil {
log.Fatal(err)
}
fmt.Println(schema.Root().Fields[0].Type.LogicalType)
}
Output: money
func NewCustomType ¶ added in v1.3.0
func NewCustomType[G, A any]( logicalType string, encode func(G, *SchemaNode) (A, error), decode func(A, *SchemaNode) (G, error), ) CustomType
NewCustomType returns a type-safe CustomType for the common case of mapping a custom Go type to/from a primitive Avro type.
G is your custom Go type (e.g. Money). A is the Avro-native Go type: int32 for int, int64 for long, float32 for float, float64 for double, string for string, []byte for bytes, bool for boolean. A may also be a named type whose underlying kind is one of these (e.g. type Cents int64); we infer the Avro type from A's kind and convert the decoded value to A.
We infer GoType and AvroType from the type parameters. If A is not a supported Avro-native type, Parse or SchemaFor returns an error.
Note that we infer AvroType from A's Go kind, which may not match the Avro schema's type for logical types backed by smaller types. For example, time-millis uses Avro "int" but time.Duration is int64, which infers "long". Use int32 as A, or use the CustomType struct directly with an explicit AvroType.
For fixed, records, or types needing extra schema metadata, use the CustomType struct directly.
Example ¶
package main
import (
"fmt"
"log"
"github.com/twmb/avro"
)
type ExMoney struct {
Cents int64
}
func main() {
// NewCustomType is the easiest way to map a custom Go type to and from a
// primitive Avro type: G is the custom Go type, A the Avro-native Go type
// it maps to, and A decides the wire type:
// int32 -> int float32 -> float bool -> boolean
// int64 -> long float64 -> double string -> string []byte -> bytes
// The first argument is the logicalType to match; "" matches all schema
// nodes of the inferred Avro type.
moneyType := avro.NewCustomType[ExMoney, int64]("money",
func(m ExMoney, _ *avro.SchemaNode) (int64, error) { return m.Cents, nil },
func(c int64, _ *avro.SchemaNode) (ExMoney, error) { return ExMoney{Cents: c}, nil },
)
schema := avro.MustParse(`{
"type": "record", "name": "Order",
"fields": [
{"name": "price", "type": {"type": "long", "logicalType": "money"}}
]
}`, moneyType)
type Order struct {
Price ExMoney `avro:"price"`
}
data, err := schema.Encode(&Order{Price: ExMoney{Cents: 1999}})
if err != nil {
log.Fatal(err)
}
var out Order
if _, err := schema.Decode(data, &out); err != nil {
log.Fatal(err)
}
fmt.Printf("%d cents\n", out.Price.Cents)
}
Output: 1999 cents
type Duration ¶
Duration is the Avro duration logical type: a 12-byte fixed value holding three little-endian uint32s, months and days and milliseconds.
func DurationFromBytes ¶ added in v1.3.0
DurationFromBytes decodes a 12-byte little-endian fixed value into a Duration, returning the zero Duration if b is shorter than 12 bytes. Use it in a CustomType Decode callback, which receives the raw []byte, to read the value before you convert it to your own type.
type Opt ¶ added in v1.3.0
type Opt interface {
// contains filtered or unexported methods
}
Opt configures encoding and decoding. Each option's doc says which functions it affects; we silently ignore an option that does not apply.
func AliasInput ¶ added in v1.9.0
func AliasInput() Opt
AliasInput makes decoded strings and byte slices point into the bytes they were read from, rather than copying them as we do by default. This applies to Schema.Decode and Schema.DecodeSingleObject. Schema.DecodeJSON ignores it, because a JSON string with an escape cannot alias.
You must not modify src after the decode, and you must not modify anything the decode returns. An aliased value *is* the memory it was read from: if you write to src, every string and byte slice decoded from it changes too, even though Go otherwise guarantees strings are immutable. Usually that memory is your src, but a field filled from a schema default aliases the parsed Schema itself, which every decode of that schema shares.
One aliased field keeps the whole buffer it points into alive for as long as you hold it. Do not use this option if you reuse the buffer, or if you keep one small field of a large message around.
We alias string and []byte targets of the string, bytes and fixed kinds, including inside an any, under a uuid logical type, and as map keys. We still copy for [N]byte, encoding.TextUnmarshaler, and any logical type that builds a new Go value (decimal, the timestamps, and the hex-dash uuid form).
This is an Opt rather than a SchemaOpt on purpose: ocf.WithSchemaOpts forwards SchemaOpts into an OCF reader, and a reader must not alias its block buffer. ocf.WithDecodeOpts drops this option for the same reason.
func LinkedinFloats ¶ added in v1.3.0
func LinkedinFloats() Opt
LinkedinFloats encodes NaN as JSON null and ±Infinity as ±1e999 in Schema.EncodeJSON, the linkedin/goavro convention, overriding our default of the JSON strings "NaN", "Infinity" and "-Infinity" (the Java convention).
Schema.DecodeJSON accepts both conventions for a float or double decoded directly or as a tagged union branch ({"float":null} decodes to NaN). Note that a NaN inside a *bare* union does not round-trip: it encodes as a bare null, and on decode the union's null branch claims that null (or the union rejects it if it has no null branch) before we try the float branch. Use TaggedUnions if you need NaN to round-trip through a union. ±Infinity is a number token and round-trips in a bare union regardless.
func SkipUnknown ¶ added in v1.9.0
func SkipUnknown() Opt
SkipUnknown allows decoding into a struct that maps only some of a record's fields, skipping the fields your struct lacks rather than erroring as we do by default. Nested records follow the same rule. This applies to Schema.Decode, Schema.DecodeJSON and Schema.DecodeSingleObject. It is decode only: encoding from a struct that does not cover the record still errors, since the missing fields would go out as zero values.
Note that an ambiguous field name (two same-depth fields claiming it) still errors. Your type has fields for it, so there is nothing to skip.
func TagLogicalTypes ¶ added in v1.3.0
func TagLogicalTypes() Opt
TagLogicalTypes qualifies union branch names with their logical type, "long.timestamp-millis" rather than the spec's "long". This is the linkedin/goavro convention. It applies to Schema.EncodeJSON and Schema.Decode, and only alongside TaggedUnions; without that option it does nothing.
func TaggedUnions ¶ added in v1.3.0
func TaggedUnions() Opt
TaggedUnions wraps non-null union values as {"type_name": value}, overriding the default of bare values.
Schema.EncodeJSON emits the tagged form. Schema.Decode and Schema.DecodeJSON wrap union values as map[string]any{branchName: value}, but only when your decode target is *any. A typed target (a concrete struct field, *T, or a non-empty interface) cannot hold the wrapper, so it gets the bare branch value.
Schema.DecodeJSON and Schema.Encode take both tagged and bare union input either way.
The tagged form is what the Avro spec defines for JSON. Java's JsonDecoder and fastavro's JSON decoder reject our bare default on the first non-null union field, so pass TaggedUnions if Java, fastavro, or avro-tools fromjson reads your output. The bare default is for goavro's bare-JSON codecs (NewCodecForStandardJSON and NewCodecForStandardJSONFull) and for plain map[string]any consumers. See AVRO-2899 for the upstream discussion.
Note that a bare union value does not name its branch, so Schema.DecodeJSON cannot tell which branch the writer used when several share a JSON token class: a bare 7 matches int, long, float and double, and a bare "x" matches string, bytes, fixed and enum. We use the *first* such branch in declaration order. That can differ from the writer's branch, and it bypasses a CustomType registered on a later branch of the same class: its Decode never runs, and a typed target is filled by plain coercion from the first branch. Binary Schema.Decode is unaffected, since the wire carries the branch index. If branch identity or a branch-bound CustomType matters to you, encode and decode with TaggedUnions.
type Schema ¶
type Schema struct {
// contains filtered or unexported fields
}
Schema is a compiled Avro schema. Create one with Parse or MustParse, then use Schema.Encode / Schema.Decode to convert between Go values and Avro binary. A Schema is safe for concurrent use.
func MustSchemaFor ¶ added in v1.1.0
MustSchemaFor is like SchemaFor but panics on error.
func Parse ¶
Parse parses an Avro JSON schema string and returns a compiled *Schema. The input can be a primitive name (e.g. `"string"`), a JSON object (record, enum, array, map, fixed), or a JSON array (union). Named types may self-reference. We fully validate: unknown types, duplicate names, invalid defaults, and so on all return errors.
To parse schemas that reference named types from other schemas, use SchemaCache.
func Resolve ¶
Resolve returns a schema that decodes data written with the writer schema and produces values matching the reader schema's layout. The writer schema is what the data was encoded with (typically from an OCF file header or a schema registry); the reader schema is what your application expects now.
Decoding with the returned schema handles field addition (defaults), field removal (skip), renaming (aliases), reordering, and type promotion. Encoding with it uses the reader's format.
We run CheckCompatibility first and return any incompatibility as a *CompatibilityError. If the check passes and the two canonical forms are identical, you get reader back as-is. The check runs first because the parsing canonical form strips logicalType, precision and scale, so two schemas with equal canonical forms can still be incompatible (a decimal precision/scale mismatch, for example), and such a pair would otherwise silently rescale the decoded value.
Note that the argument order is (writer, reader), matching the source-then-destination convention and Java's GenericDatumReader. This differs from the Avro spec text and hamba/avro, which put reader first.
Example ¶
package main
import (
"fmt"
"log"
"github.com/twmb/avro"
)
func main() {
// v1 wrote User with just a name.
writerSchema := avro.MustParse(`{
"type": "record", "name": "User",
"fields": [{"name": "name", "type": "string"}]
}`)
// v2 added an email field with a default.
readerSchema := avro.MustParse(`{
"type": "record", "name": "User",
"fields": [
{"name": "name", "type": "string"},
{"name": "email", "type": "string", "default": ""}
]
}`)
resolved, err := avro.Resolve(writerSchema, readerSchema)
if err != nil {
log.Fatal(err)
}
// Encode a v1 record (name only).
v1Data, err := writerSchema.Encode(map[string]any{"name": "Alice"})
if err != nil {
log.Fatal(err)
}
// Decode old data into the new layout; email gets the default.
type User struct {
Name string `avro:"name"`
Email string `avro:"email"`
}
var u User
if _, err := resolved.Decode(v1Data, &u); err != nil {
log.Fatal(err)
}
fmt.Printf("name=%s email=%q\n", u.Name, u.Email)
}
Output: name=Alice email=""
func SchemaFor ¶ added in v1.1.0
SchemaFor infers an Avro schema from the Go type T. T must be a struct.
We take field names from the avro struct tag, falling back to the Go field name. The following tag options are supported:
- avro:"-" excludes the field
- avro:",inline" flattens a nested struct's fields into the parent
- avro:",omitzero" is recorded but does not affect the schema
- avro:",alias=old_name" adds a field alias (repeatable)
- avro:",type-alias=old_name" adds an alias to the field's named type (record, enum, fixed; repeatable)
- avro:",default=value" sets the field's default value (must be last option; scalars only)
- avro:",timestamp-millis" overrides the logical type (also: timestamp-micros, timestamp-nanos, date, time-millis, time-micros)
- avro:",decimal(precision,scale)" sets the decimal logical type
- avro:",uuid" sets the uuid logical type
Type inference:
- bool -> boolean
- int8, int16, int32 -> int
- int, int64, uint32 -> long
- uint8, uint16 -> int
- float32 -> float
- float64 -> double
- string -> string
- []byte -> bytes
- [N]byte -> fixed (size N, name from Go type name or "fixed_N")
- *T -> ["null", T] union with default null (a pointer chain of any depth, **T or ***T, collapses to the same single nullable union)
- []T -> array
- map[string]T -> map
- struct -> record (recursive)
- time.Time -> long with timestamp-millis (override with tag)
- time.Duration -> int with time-millis (override with time-micros; a Duration is a span of time, so date and timestamp-* make no sense for it, and a large Duration overflows the narrower wire type)
- avro.Duration -> fixed(12) with the duration logical type (recognized by type; it takes no tag and does not accept one)
- *big.Rat -> requires explicit decimal(p,s) tag
- [16]byte with uuid tag -> fixed(16) with uuid logical type
- string (or text marshaler type) with uuid tag -> string with uuid logical type
Example ¶
package main
import (
"fmt"
"log"
"time"
"github.com/twmb/avro"
)
func main() {
type Event struct {
ID int64 `avro:"id"`
Name string `avro:"name,default=unnamed"`
Source string `avro:"source,default=web"`
Time time.Time `avro:"ts"`
Meta *string `avro:"meta"` // *T becomes ["null", T] union
}
schema := avro.MustSchemaFor[Event](avro.WithNamespace("com.example"))
// Encode, then decode back.
meta := "test"
data, err := schema.Encode(&Event{
ID: 1,
Name: "click",
Source: "mobile",
Time: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC),
Meta: &meta,
})
if err != nil {
log.Fatal(err)
}
var out Event
if _, err := schema.Decode(data, &out); err != nil {
log.Fatal(err)
}
fmt.Printf("id=%d name=%s source=%s meta=%s\n", out.ID, out.Name, out.Source, *out.Meta)
// Inspect the inferred schema.
root := schema.Root()
for _, f := range root.Fields {
if f.HasDefault {
fmt.Printf("field %s: default=%v\n", f.Name, f.Default)
}
}
}
Output: id=1 name=click source=mobile meta=test field name: default=unnamed field source: default=web field meta: default=<nil>
func (*Schema) AppendEncode ¶
AppendEncode appends the Avro binary encoding of v to dst. See Schema.Decode for the Go-to-Avro type mapping. On top of the types listed there, we also accept:
- encoding/json.Number for any numeric Avro type (int, long, float, double)
- RFC 3339 strings for timestamp and date logical types
- *big.Rat, big.Rat, float32, float64, encoding/json.Number, and numeric strings for decimal logical types
- encoding.TextAppender, encoding.TextMarshaler, and []byte for string types (and vice versa for encoding.TextUnmarshaler)
- string (hex-dash UUID format) for fixed(16) UUID logical types
- Tagged union maps (map[string]any{"typeName": value}) for union types, as produced by Schema.Decode with TaggedUnions
Example ¶
package main
import (
"fmt"
"log"
"github.com/twmb/avro"
)
func main() {
schema := avro.MustParse(`"string"`)
// AppendEncode reuses a buffer across calls, avoiding allocation.
var buf []byte
var err error
for _, s := range []string{"hello", "world"} {
buf, err = schema.AppendEncode(buf[:0], s)
if err != nil {
log.Fatal(err)
}
fmt.Printf("encoded %q: %d bytes\n", s, len(buf))
}
}
Output: encoded "hello": 6 bytes encoded "world": 6 bytes
func (*Schema) AppendEncodeJSON ¶ added in v1.3.0
AppendEncodeJSON is like Schema.EncodeJSON but appends to dst.
func (*Schema) AppendSingleObject ¶
AppendSingleObject appends a Single Object Encoding of v to dst: 2-byte magic, 8-byte CRC-64-AVRO fingerprint, then the Avro binary payload.
Example ¶
package main
import (
"fmt"
"log"
"github.com/twmb/avro"
)
func main() {
schema := avro.MustParse(`{
"type": "record",
"name": "Event",
"fields": [
{"name": "id", "type": "long"},
{"name": "name", "type": "string"}
]
}`)
type Event struct {
ID int64 `avro:"id"`
Name string `avro:"name"`
}
// Encode: 2-byte magic + 8-byte fingerprint + Avro payload.
data, err := schema.AppendSingleObject(nil, &Event{ID: 1, Name: "click"})
if err != nil {
log.Fatal(err)
}
// Decode.
var e Event
if _, err := schema.DecodeSingleObject(data, &e); err != nil {
log.Fatal(err)
}
fmt.Printf("id=%d name=%s\n", e.ID, e.Name)
}
Output: id=1 name=click
func (*Schema) Canonical ¶
Canonical returns the Parsing Canonical Form of the schema, stripping doc, aliases, defaults, and other non-essential attributes. The result is deterministic and matches Java's reference output byte-for-byte, so Schema.Fingerprint values are interoperable across implementations.
func (*Schema) Decode ¶
Decode reads Avro binary from src into v and returns the remaining bytes. v must be a non-nil pointer to a type compatible with the schema:
- null: any (always decodes to nil)
- boolean: bool, any
- int, long: int, int8-int64, uint8-uint64, any
- float: float32, float64, any
- double: float64, float32, any
- string: string, []byte, any; also encoding.TextUnmarshaler
- bytes: []byte, string, any
- enum: string, int/uint (ordinal), any
- fixed: [N]byte, []byte, any
- array: slice, any
- map: map[string]T, any
- union: any, *T (for ["null", T] unions), or the matched branch type
- record: struct (matched by field name or `avro` tag), map[string]any, any
When decoding into *any, primitive types become nil, bool, int32, int64, float32, float64, string, []byte, []any, or map[string]any (for records). Logical types decode to their natural Go equivalents:
- date, timestamp-millis/micros/nanos: time.Time (UTC)
- local-timestamp-millis/micros/nanos: time.Time (UTC; wall-clock fields encode/decode as if UTC, matching Java's reference impl)
- time-millis, time-micros: time.Duration
- decimal: *math/big.Rat
- uuid on string: string
- uuid on fixed(16): [16]byte
- duration: Duration
To produce JSON from decoded *any data use Schema.EncodeJSON, not a generic JSON encoder: it is schema-aware and converts these types back to their Avro representations (time.Time to epoch integers, []byte to \uXXXX strings).
Decode is liberal in what it accepts: we tolerate non-canonical input rather than rejecting it, such as a non-0/1 boolean byte that Java also reads as false. Encode is canonical, so such input round-trips to the canonical form.
func (*Schema) DecodeJSON ¶ added in v1.2.0
DecodeJSON decodes Avro JSON from src into v. We unwrap union wrappers, convert bytes/fixed strings, and coerce numeric types to match the schema. When v is *any, you get the natural Go value directly.
We accept every input format: tagged and bare unions, the Java and goavro NaN/Infinity conventions, and the linkedin/goavro union branch naming ("long.timestamp-millis" instead of "long"). Pass TaggedUnions to wrap decoded union values when the target is *any.
A bare union value whose JSON token class matches several branches (a bare number against ["long","int"], say) decodes via the first matching branch in declaration order, since the bare form does not name the writer's branch. See TaggedUnions for the consequences.
On a schema returned by Resolve, src is JSON in the writer's shape, as a producer using the writer schema would emit it. We apply full writer-to-reader resolution: promotion, enum-symbol remapping to the reader default, field add and drop, and aliases, matching Java's ResolvingDecoder over a JsonDecoder. Note that this path decodes the writer's JSON and then resolves through a binary decode, so it is slower than a plain DecodeJSON.
Example ¶
package main
import (
"fmt"
"log"
"github.com/twmb/avro"
)
func main() {
schema := avro.MustParse(`{
"type": "record",
"name": "User",
"fields": [
{"name": "name", "type": "string"},
{"name": "email", "type": ["null", "string"]}
]
}`)
type User struct {
Name string `avro:"name"`
Email *string `avro:"email"`
}
// DecodeJSON accepts both bare and tagged union formats.
var u1, u2 User
if err := schema.DecodeJSON([]byte(`{"name":"Alice","email":"a@b.com"}`), &u1); err != nil {
log.Fatal(err)
}
if err := schema.DecodeJSON([]byte(`{"name":"Bob","email":{"string":"b@c.com"}}`), &u2); err != nil {
log.Fatal(err)
}
fmt.Printf("%s: %s\n", u1.Name, *u1.Email)
fmt.Printf("%s: %s\n", u2.Name, *u2.Email)
}
Output: Alice: a@b.com Bob: b@c.com
func (*Schema) DecodeSingleObject ¶
DecodeSingleObject decodes a Single Object Encoding message into v after verifying the magic and fingerprint match this schema.
For a schema returned by Resolve, we also accept the writer's fingerprint, since single-object bytes carry the fingerprint of the schema that produced them.
func (*Schema) Encode ¶
Encode encodes v as Avro binary. It is shorthand for AppendEncode(nil, v).
Example (TextMarshaler) ¶
package main
import (
"fmt"
"log"
"net"
"github.com/twmb/avro"
)
func main() {
// Types implementing encoding.TextMarshaler are encoded as Avro
// strings, and encoding.TextUnmarshaler types decode from them.
schema := avro.MustParse(`{
"type": "record",
"name": "Server",
"fields": [
{"name": "name", "type": "string"},
{"name": "ip", "type": "string"}
]
}`)
type Server struct {
Name string `avro:"name"`
IP net.IP `avro:"ip"`
}
data, err := schema.Encode(&Server{
Name: "web-1",
IP: net.IPv4(192, 168, 1, 1),
})
if err != nil {
log.Fatal(err)
}
var out Server
if _, err := schema.Decode(data, &out); err != nil {
log.Fatal(err)
}
fmt.Printf("%s: %s\n", out.Name, out.IP)
}
Output: web-1: 192.168.1.1
func (*Schema) EncodeJSON ¶ added in v1.2.0
EncodeJSON encodes v as JSON, using the schema for type-aware encoding. By default we write union values bare and escape non-ASCII bytes/fixed bytes as \uXXXX; see Opt for the options that change the output.
We encode NaN and Infinity as the JSON strings "NaN", "Infinity" and "-Infinity" (the Java convention), or as null and ±1e999 with LinkedinFloats. A generic JSON encoder rejects non-finite floats outright; these forms keep the output valid JSON for any strict parser.
We replace each invalid byte of non-UTF-8 string content with U+FFFD, for string values and map keys at any depth. A JSON string cannot carry arbitrary bytes, so JSON is lossy for such content where Schema.Encode preserves it verbatim. Java behaves the same.
EncodeJSON accepts the same Go types as Schema.Encode. We do not sort map keys, so their output order is non-deterministic.
Note that Java's JsonDecoder, fastavro's JSON decoder and avro-tools fromjson all require the {"type_name": value} envelope and reject bare union values. Pass TaggedUnions for those tools; see its doc and AVRO-2899.
Example ¶
package main
import (
"fmt"
"log"
"github.com/twmb/avro"
)
func main() {
schema := avro.MustParse(`{
"type": "record",
"name": "User",
"fields": [
{"name": "name", "type": "string"},
{"name": "email", "type": ["null", "string"]}
]
}`)
type User struct {
Name string `avro:"name"`
Email *string `avro:"email"`
}
email := "alice@example.com"
u := User{Name: "Alice", Email: &email}
// Default: bare union values.
bare, err := schema.EncodeJSON(&u)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(bare))
// TaggedUnions: wrapped as {"type": value}.
tagged, err := schema.EncodeJSON(&u, avro.TaggedUnions())
if err != nil {
log.Fatal(err)
}
fmt.Println(string(tagged))
}
Output: {"name":"Alice","email":"alice@example.com"} {"name":"Alice","email":{"string":"alice@example.com"}}
func (*Schema) Fingerprint ¶
Fingerprint hashes the schema's canonical form with h and returns the digest. Use NewRabin for the spec's CRC-64-AVRO algorithm, or crypto/sha256 for its 256-bit recommendation.
Note that byte order matters for CRC-64-AVRO. Go writes integer hashes high byte first, as crc32, crc64, adler32 and fnv all do, so NewRabin returns the fingerprint big-endian, while Java, fastavro and the single-object header write the same 64-bit value little-endian. Compare as a uint64, or reverse the bytes. A crypto/sha256 fingerprint has no byte order and already matches Java and fastavro byte for byte.
No call returns the little-endian CRC-64-AVRO form directly. Schema.AppendSingleObject writes it into the message header, SingleObjectFingerprint reads it back, and Schema.DecodeSingleObject verifies it.
func (*Schema) Root ¶ added in v1.2.0
func (s *Schema) Root() *SchemaNode
Root returns a SchemaNode tree describing the parsed schema. We preserve all metadata: doc strings, namespaces, custom properties, numeric defaults. See SchemaNode.Props and SchemaField.Default for how values decode.
Reserved Avro attribute names ("type", "name", "namespace", "doc", "aliases", ...) match only by their exact lowercase spelling, as in the Avro reference implementations. A case variant such as "Aliases" is an ordinary custom property: it never binds the attribute, and we report it verbatim in SchemaNode.Props. Parsing applies the same rule, so a schema whose only spelling of a structural key is a case variant ("ITEMS" on an array) fails Parse, because the structural attribute is absent.
A field written in the flat goavro-style format (a bare complex type name with the kind's defining key, such as "symbols" or "items", alongside the field's own keys) appears as it parses: the field's type is the nested definition we lifted out (named after the field for record, error, enum, and fixed), and the keys we moved into the type appear on the type node rather than in SchemaField.Props. SchemaNode.Schema rebuilds the nested form, which parses identically.
Every node converts back to a usable *Schema via SchemaNode.Schema, name-reference nodes included: the tree carries the schema's named-type definitions, so any subtree you extract is self-contained.
Root re-parses the JSON on each call. Cache the result if you access it repeatedly (e.g. in a per-message loop).
type SchemaCache ¶
type SchemaCache struct {
// contains filtered or unexported fields
}
SchemaCache accumulates named types across multiple SchemaCache.Parse calls, so that a schema can reference types defined in previously parsed schemas. This is how a schema registry's inter-schema references work.
Parse schemas in dependency order: a referenced type must be parsed before the schemas that reference it.
You can parse the same schema string more than once; we return the previously parsed result, so diamond dependencies (A->B->D, A->C->D) need no tracking on your side. Options that change what the string compiles to, custom types or WithLaxNames, skip this deduplication and re-parse, since the string alone no longer identifies the result. We normalize JSON whitespace and key order when deduplicating, but not the Avro canonical form: schemas differing only in formatting dedupe, while differences in non-canonical fields like doc or aliases return a duplicate type error.
Each returned *Schema is fully resolved and independent of the cache. That extends to sub-schemas: a node extracted from Schema.Root converts via SchemaNode.Schema with every cross-parse reference resolved, so you never need the cache again once Parse returns.
Note that WithLaxNames is sticky: if a type is defined with it, pass it to every later Parse that references that type. A schema containing a lax name is not parseable without it, cache or no cache, so re-parsing the referencing schema's Schema.String or Schema.Canonical output also needs WithLaxNames. Schema.Encode and Schema.Decode are unaffected.
The zero value is ready to use. A SchemaCache is safe for concurrent use.
Example ¶
package main
import (
"fmt"
"log"
"github.com/twmb/avro"
)
func main() {
cache := new(avro.SchemaCache)
// Parse the Address type first.
if _, err := cache.Parse(`{
"type": "record",
"name": "Address",
"fields": [
{"name": "street", "type": "string"},
{"name": "city", "type": "string"}
]
}`); err != nil {
log.Fatal(err)
}
// User references Address by name.
schema, err := cache.Parse(`{
"type": "record",
"name": "User",
"fields": [
{"name": "name", "type": "string"},
{"name": "address", "type": "Address"}
]
}`)
if err != nil {
log.Fatal(err)
}
type Address struct {
Street string `avro:"street"`
City string `avro:"city"`
}
type User struct {
Name string `avro:"name"`
Address Address `avro:"address"`
}
data, err := schema.Encode(&User{
Name: "Alice",
Address: Address{Street: "123 Main St", City: "Springfield"},
})
if err != nil {
log.Fatal(err)
}
var u User
if _, err := schema.Decode(data, &u); err != nil {
log.Fatal(err)
}
fmt.Printf("%s lives at %s, %s\n", u.Name, u.Address.Street, u.Address.City)
}
Output: Alice lives at 123 Main St, Springfield
func (*SchemaCache) Parse ¶
func (c *SchemaCache) Parse(schema string, opts ...SchemaOpt) (*Schema, error)
Parse parses a schema string, registering any named types (records, enums, fixed) in the cache. Named types from previous Parse calls are available for reference resolution. On failure we do not modify the cache.
type SchemaField ¶ added in v1.2.0
type SchemaField struct {
Name string // field name
Type SchemaNode // field schema
// Default is the field's default value, present when HasDefault is
// true. The Go type matches the schema:
//
// - int schemas give int32, long schemas give int64. We reject
// out-of-range defaults at parse.
// - float schemas give float32, double float64. Overflows narrow to
// ±Inf; NaN, ±Inf, and a float-syntax "-0.0" round-trip. An
// integer-syntax "-0" is the sign-less integer 0 and reads as +0.0
// (matching Java and fastavro), though the wire encoder writes -0.0
// for it.
// - string and enum schemas give string.
// - bytes and fixed give []byte, already decoded from the JSON spec's
// codepoint-per-byte form.
// - record, array, and map give map[string]any or []any, each leaf
// following these same rules.
//
// Union defaults pick the first branch that accepts the value, and the Go
// type tells you which: ["float","int"] with default 42 gives float32(42).
//
// Unlike Props, a numeric Default is never json.Number: we reject defaults
// that do not fit the declared type.
Default any
HasDefault bool // true if a default value is defined in the schema
Aliases []string // field aliases for schema evolution
Order string // sort order: "ascending" (default), "descending", or "ignore"
Doc string // documentation string
// Props holds custom (non-reserved) field properties; numbers decode as
// in [SchemaNode.Props]. A field-level "logicalType", "precision", and
// "scale" appear here as written even when we lift them onto the
// field's type for encoding and decoding. An unused precision or scale
// is an ordinary property whatever its JSON shape; we only validate the
// pair when a decimal logicalType on a bytes or fixed field uses it.
Props map[string]any
// contains filtered or unexported fields
}
SchemaField represents a field in an Avro record schema.
type SchemaNode ¶ added in v1.2.0
type SchemaNode struct {
Type string // Avro type or named type reference
LogicalType string // e.g. date, timestamp-millis, decimal, uuid; empty if none (or if the value is not a string; see Props)
Name string // name for record, enum, fixed
// Namespace is the named type's resolved namespace. [Schema.Root] fills it
// for every named type, a child that inherits its enclosing namespace
// shows that namespace here, and "" always means the null namespace,
// never "inherit". [SchemaNode.Schema] emits a "namespace":"" escape when
// a null-namespace type sits inside a namespaced scope, so the distinction
// survives the round trip. A dotted Name takes precedence over this field.
Namespace string
Aliases []string // alternate names for named types (record, enum, fixed)
Doc string // documentation string
Fields []SchemaField // record fields
Items *SchemaNode // array element schema
Values *SchemaNode // map value schema
Branches []SchemaNode // union member schemas
Symbols []string // enum symbols
Size int // fixed byte size
EnumDefault string // default symbol for enum schema evolution
HasEnumDefault bool // true if an enum default is defined
// Precision and Scale are the decimal logical type's parameters. We set
// and validate them only when LogicalType is "decimal" on a bytes or
// fixed type. Anywhere else (no logical type, an unknown or non-decimal
// one, or a decimal on a type that does not support it) the attributes
// are plain metadata and appear in Props, as at the field level.
Precision int // decimal precision
Scale int // decimal scale
// Props holds custom (non-reserved) schema attributes: anything in the
// schema JSON that is not a standard Avro field (e.g. "com.example.tag").
// A reserved structural key on a kind that does not use it ("items" on
// an "int") also appears here when its body does not parse as a schema
// (a stray "items":3), and the matching structural field stays zero. A
// schema-shaped stray body instead appears as written on Items, Values,
// or Fields. A non-string logicalType likewise appears here verbatim,
// since only a string can name a logical type.
//
// Values use the natural Go types from JSON: string, bool, nil, []any,
// map[string]any, int64 for whole numbers, float64 for fractional. A
// number stays json.Number when neither fits: a whole number too large
// for int64, or a fractional literal over 1024 bytes, whose digits are
// kept verbatim rather than rounded. Whole-valued exponents collapse to
// int64 (1e3 reads as int64(1000)); exponents overflowing float64 give
// ±Inf. math.NaN() re-reads as the string "NaN" after Schema()/Root(),
// because JSON has no NaN literal; ±Inf round-trips as float64(±Inf).
//
// When you build a node by hand, a map key with a MarshalText method
// renders as that text whatever the key's kind, a float-kind key is an
// error, and invalid UTF-8 in a string or key becomes U+FFFD. None of
// this depends on the Go version. encoding/json changed all three
// between Go 1.26 and Go 1.27, so we name keys and replace bytes
// ourselves before it runs.
Props map[string]any
// contains filtered or unexported fields
}
SchemaNode is a read-write representation of an Avro schema. You get one from a parsed schema via Schema.Root, or you build one directly and convert it with SchemaNode.Schema.
The Type field determines which other fields are relevant:
- Primitives (null, boolean, int, long, float, double, string, bytes): LogicalType, Precision, Scale, Props optional; other fields ignored.
- record/error: Name, Fields required; Namespace, Doc, Props optional.
- enum: Name, Symbols required; Namespace, Doc, Props optional.
- array: Items required.
- map: Values required.
- fixed: Name, Size required; LogicalType, Precision, Scale, Namespace, Props optional.
- union: Branches lists the member schemas.
To reference a named type (record, enum, fixed) defined elsewhere in the schema, set Type to its full name (e.g. com.example.Address) and nothing else. In a Schema.Root tree, references also resolve outward. Converting *any* node with SchemaNode.Schema resolves names against the schema the tree came from, so a field type, union branch, or deeper node converts even when the definition lives outside the extracted node. A hand-built tree has no enclosing schema, so there you must define every referenced name within the tree you convert, or Schema returns an error.
func (*SchemaNode) ExpandReferences ¶ added in v1.9.0
func (n *SchemaNode) ExpandReferences() *SchemaNode
ExpandReferences returns a copy of n's tree with every name reference replaced by the definition it names, so each occurrence of a repeated named type carries the full body rather than only the first. n is not modified.
A reference resolves the way SchemaNode.Schema resolves it, so a subtree extracted from a Schema.Root tree expands even when the definition lives outside it.
Some references stay as they are. A name that closes a cycle never expands, since expanding a recursive definition does not terminate. A reference that carries attributes of its own ({"type":"Inner","doc":"x"}) stays, because a definition cannot hold a second doc. And if the fully expanded tree would exceed an internal ceiling, nothing expands at all, since a chain of definitions each naming the previous twice doubles per level; we return an unexpanded copy rather than a partial one.
SchemaNode.Schema collapses repeats back to references on emit, so n.ExpandReferences().Schema() and n.Schema() produce the same schema. It spells a collapsed repeat by fullname, so a reference you wrote as an in-scope short name comes back qualified.
func (*SchemaNode) Schema ¶ added in v1.2.0
func (n *SchemaNode) Schema(opts ...SchemaOpt) (*Schema, error)
Schema parses the SchemaNode into a *Schema you can encode and decode with. We return an error if the node is invalid.
Named types appearing multiple times are deduplicated by fullname: the first occurrence emits the definition, later ones emit the fullname as a reference. Two types sharing a short name across namespaces are distinct and both emit definitions.
A node extracted from a Schema.Root tree may reference definitions living elsewhere in the enclosing schema: an earlier field, a prior SchemaCache parse, or the enclosing type itself for a recursive schema. Those resolve automatically. We emit the definition at the reference's first occurrence, so the result needs neither the enclosing schema nor any cache. A name the tree defines itself wins over the enclosing schema's definition. Custom properties on a wrapped reference move onto the emitted definition, while reserved usage-site attributes (doc, namespace) do not survive. Hand-built nodes have no enclosing schema, so there a reference the tree does not define is an error.
opts pass through to the internal Parse. If you parsed the original schema with [SchemaOpt]s that change what Parse accepts or wires (WithLaxNames, CustomType registrations), pass the same opts here. Otherwise the rebuilt schema fails to parse or silently lacks the custom wiring.
type SchemaOpt ¶ added in v1.1.0
type SchemaOpt interface {
// contains filtered or unexported methods
}
SchemaOpt configures schema construction via Parse, SchemaCache.Parse, or SchemaFor. We silently ignore an option that does not apply.
func WithCustomType ¶ added in v1.3.0
func WithCustomType(ct CustomType) SchemaOpt
WithCustomType registers a custom type conversion for Parse, SchemaCache.Parse, or SchemaFor. CustomType and NewCustomType satisfy SchemaOpt directly, so this wrapper is optional.
func WithLaxNames ¶
WithLaxNames relaxes name validation in Parse and SchemaCache.Parse, overriding our default of the Avro strict name regex [A-Za-z_][A-Za-z0-9_]*. A nil fn requires only non-empty names; otherwise we split dot-separated fullnames and call fn for each name component, and you return an error for the names you reject. SchemaFor ignores this option.
type SemanticError ¶
type SemanticError struct {
// GoType is the Go type involved, if applicable.
GoType reflect.Type
// AvroType is the Avro schema type (e.g. "int", "record", "boolean").
AvroType string
// Field is the dotted path to the record field (e.g. "address.zip"),
// if the error occurred within a record.
Field string
// Err is the underlying error.
Err error
}
SemanticError indicates a Go type is incompatible with an Avro schema type during encoding or decoding.
func (*SemanticError) Error ¶
func (e *SemanticError) Error() string
func (*SemanticError) Unwrap ¶
func (e *SemanticError) Unwrap() error
type ShortBufferError ¶
type ShortBufferError struct {
// Type is what was being read (e.g. "boolean", "string", "uint32").
Type string
// Need is the number of bytes required (0 if unknown).
Need int
// Have is the number of bytes available.
Have int
}
ShortBufferError indicates the input buffer is too short for the value being decoded.
func (*ShortBufferError) Error ¶
func (e *ShortBufferError) Error() string
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package atype names the Avro schema types, logical types, and field sort orders.
|
Package atype names the Avro schema types, logical types, and field sort orders. |
|
internal
|
|
|
optmark
Package optmark marks avro decode options by a property the code hosting a decode has to act on.
|
Package optmark marks avro decode options by a property the code hosting a decode has to act on. |
|
Package ocf implements Avro [Object Container Files] (OCF).
|
Package ocf implements Avro [Object Container Files] (OCF). |