Apache Avro's logical types, extended: introducing vanavro

Avro schemas describe the shape of data, not whether it makes sense. A string field will hold a broken URL; a latitude of 512 degrees is still a valid double. We wanted Rust types that refuse such data at the boundary, generated from our Avro IDL schemas. The generator we evaluated needed Apache Avro's Java implementation to read IDL, lacked namespaces, could not validate values, and is unmaintained. So we created one.

We have open-sourced vanavro under your choice of the Apache 2.0 or MIT licence. It turns .avsc and .avdl schemas into idiomatic Rust types, supporting records, enums, unions, recursive types, namespaces, defaults, and aliases. We test every feature against Avro's reference Java implementation: it decodes and re-encodes data written by the generated Rust types, and Rust checks the result.

Where Avro's (logical) types stop

Avro's standard logical types, such as uuid and date, add meaning to an underlying type, and the specification leaves room for custom ones. The Java implementation has supported those for years through logical type factories and conversions.

vanavro brings the same idea to Rust, with serde doing the conversion instead of runtime-registered classes. You annotate the schema as you would for a standard logical type:

@logicalType("wgs84")
record Wgs84Coordinate { double latitude; double longitude; double altitude; }

record UserProfile {
    @logicalType("uuid") string id;      // standard
    @logicalType("url")  string website; // custom
    Wgs84Coordinate location;            // custom, on the record
}

Then you tell vanavro which Rust type each annotation stands for:

VanAvro::builder()
    .idl_path("schemas/example.avdl")
    .with_logical_type(
        // Url (de)serializes as a plain string, so no converter is needed
        CustomLogicalType::builder("url", AvroType::String, "::url::Url").build(),
    )
    .with_logical_type(
        CustomLogicalType::builder("wgs84", AvroType::Record, "crate::geo::Wgs84")
            .converter("crate::geo::Wgs84AsRecord")
            .build(),
    )
    .generate_to_path(out_dir.join("example.rs"))?;

The generated code uses your types (attributes trimmed):

#[serde_with::serde_as]
pub struct UserProfile {
    pub id: ::uuid::Uuid,
    pub website: ::url::Url,
    #[serde_as(as = "crate::geo::Wgs84AsRecord")]
    pub location: crate::geo::Wgs84,
}

A UserProfile cannot hold a malformed URL, because Url cannot: its deserialiser rejects anything invalid. Where a type needs help reading the wire format, a serde_with converter handles the translation and can enforce any rule you choose, such as the range of a latitude:

impl<'de> DeserializeAs<'de, Wgs84> for Wgs84AsRecord {
    fn deserialize_as<D: Deserializer<'de>>(d: D) -> Result<Wgs84, D::Error> {
        // The wire shape of the `Wgs84Coordinate` record
        #[derive(Deserialize)]
        #[serde(rename = "Wgs84Coordinate")]
        struct Wgs84Record { latitude: f64, longitude: f64, altitude: f64 }

        let Wgs84Record { latitude, longitude, altitude } =
            Wgs84Record::deserialize(d)?;
        // `Wgs84::new` rejects a latitude outside −90 to 90
        Wgs84::new(latitude, longitude, altitude).map_err(de::Error::custom)
    }
}

Either way, invalid data fails as it is decoded rather than wherever it is first used. The check lives in your Rust types, not in the schema. Avro schema readers must ignore logical types they do not recognise. A consumer without your mappings, such as a plain Java reader, still sees the underlying record, strings, and doubles, and keeps working.

Same schema, same bytes

Custom logical types sit next to the standard ones in the same schema, in almost any position a type can appear. Annotate a whole record, as with Wgs84Coordinate, and vanavro generates no struct for it; every reference becomes your type instead. Nothing changes on the wire, and the embedded schema still describes the record in full.

The schemas stay portable, and the Rust side refuses bad data the moment it arrives.

If you find this interesting, we encourage you to try vanavro. We look forward to your feedback and contributions!