avro-js and avsc are both JavaScript implementations of the Apache Avro specification, designed for serializing and deserializing structured data using binary encoding. avsc is a comprehensive, pure-JavaScript library that supports the full Avro spec, including schema resolution, logical types, and fingerprinting, making it suitable for both Node.js and browser environments. avro-js is an older implementation that provides core encoding and decoding capabilities but has seen less active development and feature parity compared to avsc in recent years.
When working with Apache Avro in the JavaScript ecosystem, you need a library that can handle schema definition, binary encoding, and decoding reliably. avsc and avro-js are the two primary options, but they differ significantly in maintenance, feature completeness, and API design. Let's break down how they handle real-world serialization tasks.
avsc provides a robust API for parsing schemas and creating types. It supports schema resolution and evolution out of the box.
// avsc: Parse schema and create type
const avsc = require('avsc');
const schema = {
type: 'record',
name: 'User',
fields: [
{ name: 'id', type: 'long' },
{ name: 'username', type: 'string' }
]
};
const type = avsc.Type.forSchema(schema);
const value = { id: 123, username: 'alice' };
const buffer = type.toBuffer(value);
avro-js uses a similar concept but with a different API structure. It requires creating a schema object first.
// avro-js: Create schema and encode
const avro = require('avro-js');
const schema = avro.schema({
type: 'record',
name: 'User',
fields: [
{ name: 'id', type: 'long' },
{ name: 'username', type: 'string' }
]
});
const value = { id: 123, username: 'alice' };
const buffer = schema.toBuffer(value);
avsc handles encoding and decoding with built-in validation. It throws clear errors if the data doesn't match the schema.
// avsc: Decode with validation
const decoded = type.fromBuffer(buffer);
// decoded: { id: 123, username: 'alice' }
// Invalid data throws an error
try {
type.fromBuffer(Buffer.from('invalid'));
} catch (err) {
console.error(err.message); // Clear error message
}
avro-js also supports encoding and decoding but may have less detailed error messages in older versions.
// avro-js: Decode data
const decoded = schema.fromBuffer(buffer);
// decoded: { id: 123, username: 'alice' }
// Error handling varies by version
try {
schema.fromBuffer(Buffer.from('invalid'));
} catch (err) {
console.error(err.message);
}
avsc has first-class support for logical types (e.g., dates, decimals) and complex unions.
// avsc: Logical types support
const schemaWithLogical = {
type: 'record',
name: 'Event',
fields: [
{ name: 'timestamp', type: { type: 'long', logicalType: 'timestamp-millis' } }
]
};
const type = avsc.Type.forSchema(schemaWithLogical);
const buffer = type.toBuffer({ timestamp: new Date() });
const event = type.fromBuffer(buffer); // timestamp is a Date object
avro-js supports logical types but may require additional configuration or have limited support for newer logical types depending on the version.
// avro-js: Logical types support
const schemaWithLogical = avro.schema({
type: 'record',
name: 'Event',
fields: [
{ name: 'timestamp', type: { type: 'long', logicalType: 'timestamp-millis' } }
]
});
const buffer = schemaWithLogical.toBuffer({ timestamp: new Date() });
const event = schemaWithLogical.fromBuffer(buffer);
avsc excels at schema evolution. It allows you to decode data written with an older schema using a new schema (reader/writer schema resolution).
// avsc: Schema resolution
const writerType = avsc.Type.forSchema(writerSchema);
const readerType = avsc.Type.forSchema(readerSchema);
const buffer = writerType.toBuffer({ id: 1, username: 'alice', email: 'a@example.com' });
// Decode with reader schema (missing fields handled gracefully)
const value = readerType.fromBuffer(buffer, writerType);
avro-js supports schema evolution but the API for reader/writer resolution is less documented and may require manual handling in some cases.
// avro-js: Schema resolution
// Similar concept but API may differ slightly
const buffer = writerSchema.toBuffer(data);
const value = readerSchema.fromBuffer(buffer, writerSchema);
avsc is designed to work in both Node.js and browser environments. It has no native dependencies.
// avsc: Browser usage
// Works with bundlers like Webpack or Vite
import * as avsc from 'avsc';
// No special configuration needed for most cases
avro-js also works in browsers but may require polyfills for older environments or specific bundler configurations.
// avro-js: Browser usage
import * as avro from 'avro-js';
// May need additional setup for certain bundlers
avsc includes TypeScript definitions, making it easier to use in typed projects.
// avsc: TypeScript usage
import * as avsc from 'avsc';
const schema: avsc.Schema = {
type: 'record',
name: 'User',
fields: [{ name: 'id', type: 'long' }]
};
const type = avsc.Type.forSchema(schema);
avro-js has community-maintained types or older definitions that may not cover all APIs.
// avro-js: TypeScript usage
import * as avro from 'avro-js';
// Types may be less complete than avsc
| Feature | avsc | avro-js |
|---|---|---|
| Maintenance | ✅ Active | ⚠️ Slower updates |
| Schema Resolution | ✅ Full support | ✅ Supported |
| Logical Types | ✅ Comprehensive | ✅ Basic support |
| TypeScript | ✅ Built-in types | ⚠️ Community types |
| Browser Ready | ✅ Yes | ✅ Yes |
| API Clarity | ✅ Modern | ⚠️ Legacy feel |
avsc is the clear choice for modern JavaScript development. It offers better maintenance, clearer APIs, and more robust support for advanced Avro features like schema evolution and logical types. Its TypeScript support makes it ideal for large-scale frontend or full-stack applications.
avro-js is a viable option only for legacy projects where migration is too costly. For any new work, the ecosystem has standardized around avsc due to its reliability and feature completeness.
Final Thought: Data serialization is critical for interoperability. Choosing a well-maintained library like avsc reduces the risk of bugs and ensures compatibility with the broader Apache Avro ecosystem.
Choose avro-js only if you are maintaining a legacy system that already depends on it and migration costs are prohibitive. For new development, it is generally not recommended due to slower update cycles and fewer modern features compared to avsc.
Choose avsc for new projects requiring full Avro specification support, active maintenance, and robust TypeScript definitions. It is the industry standard for JavaScript Avro implementations, offering advanced features like schema evolution, logical types, and fingerprinting that are critical for production data pipelines.
Pure JavaScript implementation of the Avro specification.
avro-js even runs in the browser.$ npm install avro-js
avro-js is compatible with all versions of node.js since 0.11 and major
browsers via browserify.
See doc/ folder.
Inside a node.js module, or using browserify:
var avro = require('avro-js');
Encode and decode objects:
// We can declare a schema inline:
var type = avro.parse({
name: 'Pet',
type: 'record',
fields: [
{name: 'kind', type: {name: 'Kind', type: 'enum', symbols: ['CAT', 'DOG']}},
{name: 'name', type: 'string'}
]
});
var pet = {kind: 'CAT', name: 'Albert'};
var buf = type.toBuffer(pet); // Serialized object.
var obj = type.fromBuffer(buf); // {kind: 'CAT', name: 'Albert'}
Generate random instances of a schema:
// We can also parse a JSON-stringified schema:
var type = avro.parse('{"type": "fixed", "name": "Id", "size": 4}');
var id = type.random(); // E.g. Buffer([48, 152, 2, 123])
Check whether an object fits a given schema:
// Or we can specify a path to a schema file (not in the browser):
var type = avro.parse('./Person.avsc');
var person = {name: 'Bob', address: {city: 'Cambridge', zip: '02139'}};
var status = type.isValid(person); // Boolean status.
Get a readable stream of decoded records from an Avro container file (not in the browser):
avro.createFileDecoder('./records.avro')
.on('metadata', function (type) { /* `type` is the writer's type. */ })
.on('data', function (record) { /* Do something with the record. */ });
Implement recursive schemata (due to lack of duck-typing):
// example type: linked list with one long-int as element value
const recursiveRecordType = avro.parse({
"type": "record",
"name": "LongList",
"fields" : [
{"name": "value", "type": "long"},
{"name": "next", "type": ["null", "LongList"]} // optional next element via recursion
]
});
// will work
const validRecursiveRecordDTO = {
value: 1,
next: {
// no duck-typing support: from first nested level on the
// recursive type has to be explicitly specified.
LongList: {
value: 2,
next: null
}
}
};
const serializedValid = recursiveRecordType.parse(validRecursiveRecordDTO);
// will throw error
const invalidRecursiveRecordDTO = {
value: 1,
next: {
value: 2,
next: null
}
};
const serializedInvalid = recursiveRecordType.parse(invalidRecursiveRecordDTO);