Apache Avro: Module 1 Section One - Environment Setup

Apache Avro in Java - Setting Up a Maven Project That Generates Code from Schemas
- What is Apache Avro?
- How the Build Fits Together
- Where Schemas and Generated Code Live
- Project Configuration
- Build Walkthrough
- Testing the Build Itself
- Common Pitfalls
- Wrapping Up
Most Java libraries arrive as a single dependency and you start calling them. Apache Avro is different: the centre of an Avro project is not Java code but a schema, a JSON file that describes your data. From that schema a build plugin generates Java classes, and your code, your tests and every system that reads your data depend on the schema being where the build expects it. Get the project layout right and Avro feels effortless. Get it wrong and you end up with generated code committed next to hand-written code, edits that vanish on the next build, and readers that refuse to load your own classes.
This guide sets up the Maven project behind the Getting Started (Java) guide of Apache Avro 1.12.2, the first section of an example-driven Avro course in which every concept is a JUnit 6 test. You’ll add the dependency and the avro-maven-plugin, decide where schemas and generated code live and why, run the build, look at what it generated, and write tests that check the build itself.
What is Apache Avro?
Apache Avro is a data serialization system. You describe your data once, in a schema, and Avro uses it to write that data in a compact binary form and read it back. Three properties make it popular in data platforms:
- The schema travels with the data. A data file stores its schema in its header, so any reader can decode it years later.
- The binary form is small. There are no field names or type tags in the data, only values in schema order.
- Schemas can evolve. Data written with one version of a schema can be read with another, so producers and consumers can change at different speeds.
In Java, Avro offers two ways to work with data: generated classes (a User class with getters, setters and a builder, produced from the schema at build time) and generic records (a map-like object driven by a schema loaded at runtime). This first section sets up the build for the generated-class approach; section 1.7 covers the generic one.
How the Build Fits Together
An Avro project adds one step in front of the normal Maven build:
generate-sources: theavro-maven-pluginreads every.avscfile in its source directory and writes one Java class per named type.process-resources: Maven copiessrc/main/resourcestotarget/classes. If the schemas live there, they end up on the classpath too.compile: the compiler compiles your code and the generated classes, because the plugin registers its output directory as a source root.test: your tests use the generated classes like any other.
Because generation happens on every build, the schema is the single source of truth. Change user.avsc, rebuild, and the User class changes with it.
Where Schemas and Generated Code Live
The Getting Started guide and this course make two different choices here, and the difference matters in real projects.
The Guide’s Layout
The guide keeps schemas in the plugin’s default directory, src/main/avro, and sets the plugin’s outputDirectory to src/main/java. The generated User.java then sits in your source tree next to the code you write. That makes the first example simple, because your IDE sees the class without a build, but it has a cost.
This Course’s Layout
The course makes two changes:
- Schemas live in
src/main/resources/avro. The plugin reads them from there, and because they’re resources, Maven also copies them totarget/classes/avro. Your code can load a schema from the classpath at runtime, and it ships inside the jar. - Generated code stays in
target/generated-sources/avro, the plugin’s default output directory. It is build output:mvn cleandeletes it,.gitignorekeeps it out of Git, and nobody edits it by accident.
Why Generated Code Should Never Be Edited
Every generated file starts with this header:
/*
* Autogenerated by Avro
*
* DO NOT EDIT DIRECTLY
*/
The warning is there for a reason. The plugin only regenerates a class when the schema file is newer than the generated file. A hand edit to User.java in src/main/java therefore survives build after build, until someone changes user.avsc, and then it disappears without an error. It can also leave the class disagreeing with the schema it embeds. Keeping generated code in target/ removes the temptation: there is nothing to edit that would last.
If you need a different generated class, change the schema, change a plugin option (such as stringType), or write code around the class rather than in it.
Project Configuration
The course is a multi-module Maven project, java-apache-avro, with one module per topic. Module 1 is the Getting Started guide.
Step 1: Create the Parent POM
The parent manages versions for every module and passes one Avro 1.12.2 setting to every test run (Step 4 explains it):
<groupId>com.example</groupId>
<artifactId>java-apache-avro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>module-01</module>
</modules>
<properties>
<java.version>21</java.version>
<maven.compiler.release>${java.version}</maven.compiler.release>
<avro.version>1.12.2</avro.version>
<junit.version>6.1.3</junit.version>
<assertj.version>3.27.7</assertj.version>
<avro.trusted.packages>example.avro</avro.trusted.packages>
</properties>
Its dependencyManagement imports the JUnit BOM and pins avro and assertj-core; its pluginManagement pins the compiler, Surefire and avro-maven-plugin versions.
Step 2: Add the Module POM
The module declares the one dependency the guide asks for, plus JUnit and AssertJ for the tests, and runs the plugin’s schema goal:
<dependencies>
<dependency>
<groupId>org.apache.avro</groupId>
<artifactId>avro</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.avro</groupId>
<artifactId>avro-maven-plugin</artifactId>
<executions>
<execution>
<phase>generate-sources</phase>
<goals>
<goal>schema</goal>
</goals>
<configuration>
<sourceDirectory>${project.basedir}/src/main/resources/avro/</sourceDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
There is no outputDirectory: the default, target/generated-sources/avro, is the point. There is no build-helper-maven-plugin either; the Avro plugin adds its output to the compile sources itself.
Step 3: Add the Schema
Save the guide’s schema as module-01/src/main/resources/avro/user.avsc:
{"namespace": "example.avro",
"type": "record",
"name": "User",
"fields": [
{"name": "name", "type": "string"},
{"name": "favorite_number", "type": ["int", "null"]},
{"name": "favorite_color", "type": ["string", "null"]}
]
}
Section 1.2 takes it apart field by field.
Step 4: Trust Your Generated Classes
Avro 1.12.2 only instantiates Java classes it trusts when it reads data, and that includes your own generated classes. Without a setting, reading User objects back from a file throws a SecurityException. The parent POM passes the package to every test run through Surefire:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<org.apache.avro.SERIALIZABLE_PACKAGES>${avro.trusted.packages}</org.apache.avro.SERIALIZABLE_PACKAGES>
</systemPropertyVariables>
</configuration>
</plugin>
Section 1.6 shows the exception and the reason behind it.
Build Walkthrough
Running the Build
mvn test -pl module-01
The first lines of the log show the extra step and the schema being copied as a resource:
[INFO] --- avro:1.12.2:schema (default) @ module-01 ---
[INFO] --- resources:3.3.1:resources (default-resources) @ module-01 ---
[INFO] Copying 1 resource from src/main/resources to target/classes
[INFO] --- compiler:3.15.0:compile (default-compile) @ module-01 ---
[INFO] Compiling 4 source files with javac [debug parameters release 21] to target/classes
“4 source files” is the three hand-written classes of Module 1 plus the generated User.java.
What the Plugin Generated
ls module-01/target/generated-sources/avro/example/avro/
User.java
The namespace example.avro became the package and the record name User became the class. The file is 541 lines long and starts like this:
/*
* Autogenerated by Avro
*
* DO NOT EDIT DIRECTLY
*/
package example.avro;
@org.apache.avro.specific.AvroGenerated
public class User extends org.apache.avro.specific.SpecificRecordBase implements org.apache.avro.specific.SpecificRecord {
public static final org.apache.avro.Schema SCHEMA$ = JsonSchemaParser.parseInternal("{\"type\":\"record\",\"name\":\"User\",...}");
public static org.apache.avro.Schema getClassSchema() { return SCHEMA$; }
Section 1.3 walks through the rest of the class.
Run every example yourself All the code in this post is in Module 1 of the Java Apache Avro course: a Maven project where every Avro concept is a JUnit 6 test you can run, break and fix. Get the code on GitHub → · Read the course outline →
Testing the Build Itself
A build configuration is code too, so Module 1 tests it. Maven runs tests with the module directory as the working directory, so paths are relative to module-01:
class ProjectSetupTest {
@Test
void theUserClassIsGeneratedDuringTheBuild() {
// If this compiles, the plugin generated the class before the test sources were compiled.
assertThat(User.getClassSchema().getFullName()).isEqualTo("example.avro.User");
assertThat(SpecificRecord.class).isAssignableFrom(User.class);
}
@Test
void theGeneratedSourceIsBuildOutput() {
assertThat(Path.of("target/generated-sources/avro/example/avro/User.java")).isRegularFile();
// The guide's outputDirectory, src/main/java, would mix generated code with code you write and commit.
assertThat(Path.of("src/main/java/example/avro/User.java")).doesNotExist();
}
@Test
void theSchemaFileIsWhereThePluginLooksForIt() {
assertThat(Path.of("src/main/resources/avro/user.avsc")).isRegularFile();
assertThat(Path.of("src/main/avro")).doesNotExist();
}
@Test
void theSchemaIsAlsoOnTheClasspath() {
assertThat(Path.of("target/classes/avro/user.avsc")).isRegularFile();
assertThat(getClass().getClassLoader().getResource("avro/user.avsc")).isNotNull();
}
}
The second test is the important one. If anyone switches the plugin’s output to src/main/java “to make the IDE happy”, the build fails and says why.
Common Pitfalls
- Generating into
src/main/java. It invites edits that the next schema change silently discards. Keep the default output directory. - Adding
build-helper-maven-plugin. Older tutorials add it to register the generated sources. The Avro plugin already does that. - Putting schemas somewhere the plugin doesn’t look. If
sourceDirectoryand the files disagree, the build succeeds but generates nothing, and the compile fails on a missingUserclass. The location test above catches it first. - Forgetting the trust setting. Writing works without it; reading generated classes fails with a
SecurityException. Setorg.apache.avro.SERIALIZABLE_PACKAGESfor tests and for the application.
Wrapping Up
An Avro project is a schema-first project. The schema lives in src/main/resources/avro, where both the plugin and the classpath can see it; the generated class lives in target/generated-sources/avro, where nobody can edit it; and one system property tells Avro 1.12.2 that your generated package is safe to instantiate. With four small tests guarding that layout, the build can’t quietly drift away from it.
Next, section 1.2 opens the schema itself: records, fields, and the unions with null that make a field optional.
Run every example yourself Clone the repository, run
mvn test -pl module-01, and watch 41 tests pass. Then change the plugin’s output directory tosrc/main/javaand watch one of them fail. Get the code on GitHub → · Module 1 README →