Kotlin has a concise syntax to declare data classes:

data class User(val name: String, val age: Int)

The equivalent Java syntax is verbose. You have to create a Java class with private fields. And getter and setter methods for the fields. And additional methods like equals(), hashCode() and toString().

But who says you have to create the Java code by hand?

In this article, I’ll show you how to generate Java source files from a YAML file.

Here’s the example YAML file:

User:
    name: Name
    age: Integer

Name:
    firstName: String
    lastName: String

The example output of the code generator is two Java source files, User.java and Name.java.

public class User{
    private Name name;
    private Integer age;
    
    public User(){
    }
    public Name getName(){
        return name;
    }
    public void setName(Name name){
        this.name = name;
    }
    public Integer getAge(){
        return age;
    }
    public void setAge(Integer age){
        this.age = age;
    }
}

Name.java is similar.

The point of this article is: You’ll learn how to program a code generator from scratch. And it’s easy to adapt it to your needs.

The main method

The main() method does two things:

  • Step 1: Read in the YAML file, into class specifications
  • Step 2: Generate Java source files from the class specifications

It decouples reading and generating. You can change the input format in the future, or support more input formats.

Here’s the main() method:

public static void main(String[] args) throws Exception {
    // Make sure there is exactly one command line argument, 
    // the path to the YAML file
    if (args.length != 1) {
        System.out.println("Please supply exactly one argument, the path to the YAML file.");
        return;
    }
  
    // Get the YAML file's handle & the directory it's contained in
    // (generated files will be placed there)
    final String yamlFilePath = args[0];
    final File yamlFile = new File(yamlFilePath);
    final File outputDirectory = yamlFile.getParentFile();
  
    // Step 1: Read in the YAML file, into class specifications
    YamlClassSpecificationReader yamlReader = new YamlClassSpecificationReader();
    List<ClassSpecification> classSpecifications = yamlReader.read(yamlFile);
    
    // Step 2: Generate Java source files from class specifications
    JavaDataClassGenerator javaDataClassGenerator = new JavaDataClassGenerator();
    javaDataClassGenerator.generateJavaSourceFiles(classSpecifications, outputDirectory);
    
    System.out.println("Successfully generated files to: " + outputDirectory.getAbsolutePath());
}

Step 1: Read the YAML file into class specifications

Let me explain what happens in this line:

List<ClassSpecification> classSpecifications =  yamlReader.read(yamlFile);

A class specification is a definition of a class to be generated and its fields.
Remember the User in the example YAML file?

User:
    name: Name
    age: Integer

When the YAML reader reads that, it will create one ClassSpecification object, with the name User. And that class specification will reference two FieldSpecification objects, called name and age.

The code for the ClassSpecification class and the FieldSpecification class is simple.

The content of ClassSpecification.java is shown below:

public class ClassSpecification {
    private String name;
    private List<FieldSpecification> fieldSpecifications;
  
    public ClassSpecification(String className, List<FieldSpecification> fieldSpecifications) {
        this.name = className;
        this.fieldSpecifications = fieldSpecifications;
    }
  
    public String getName() {
        return name;
    }
  
    public List<FieldSpecification> getFieldSpecifications() {
        return Collections.unmodifiableList(fieldSpecifications);
    }
}

The content of FieldSpecification.java is:

public class FieldSpecification {
    private String name;
    private String type;
  
    public FieldSpecification(String fieldName, String fieldType) {
        this.name = fieldName;
        this.type = fieldType;
    }
  
    public String getName() {
        return name;
    }
  
    public String getType() {
        return type;
    }
}

The only remaining question for Step 1 is: how do you get from a YAML file to objects of these classes?

The YAML reader uses the SnakeYAML library to parse YAML files.
SnakeYAML makes a YAML file’s content available in data structures like maps and lists.

For this article, you only need to understand maps — because that’s what we use in the YAML files.

Look at the example again:

User:
    name: Name
    age: Integer

Name:
    firstName: String
    lastName: String

What you see here is two nested maps.

The key of the outer map is the class name (like User).

When you get the value for the User key, you get a map of the class fields:

name: Name
age: Integer

The key of this inner map is the field name, and the value is the field type.

It’s a map of strings to a map of strings to strings. That’s important to understand the code of the YAML reader.

Here’s the method that reads in the complete YAML file contents:

private Map<String, Map<String, String>> readYamlClassSpecifications(Reader reader) {
	Yaml yaml = new Yaml();

	// Read in the complete YAML file to a map of strings to a map of strings to strings
	Map<String, Map<String, String>> yamlClassSpecifications = 
		(Map<String, Map<String, String>>) yaml.load(reader);

	return yamlClassSpecifications;
}

With the yamlClassSpecifications as input, the YAML reader creates the ClassSpecification objects:

private List<ClassSpecification> createClassSpecificationsFrom(Map<String, Map<String, String>> yamlClassSpecifications) {
	final Map<String, List<FieldSpecification>> classNameToFieldSpecificationsMap 
		= createClassNameToFieldSpecificationsMap(yamlClassSpecifications);

	List<ClassSpecification> classSpecifications = 
		classNameToFieldSpecificationsMap.entrySet().stream()
			.map(e -> new ClassSpecification(e.getKey(), e.getValue()))
			.collect(toList());

	return classSpecifications;
}

The createClassNameToFieldSpecificationsMap() method creates

  • the field specifications for each class, and based on these
  • a map of each class name to its field specifications.

Then the YAML reader creates a ClassSpecification object for each entry in that map.

The contents of the YAML file are now available to Step 2 in a YAML independent way. We’re done with Step 1.

Step 2: Generate Java source files from the class specifications

Apache FreeMarker is a Java template engine that produces textual output. Templates are written in the FreeMarker Template Language (FTL). It allows static text to mix with the content of Java objects.

Here’s the template to generate the Java source files, javadataclass.ftl:

public class ${classSpecification.name}{
<#list classSpecification.fieldSpecifications as field>
    private ${field.type} ${field.name};
</#list>
    public ${classSpecification.name}(){
    }
<#list classSpecification.fieldSpecifications as field>
    public ${field.type} get${field.name?cap_first}(){
        return ${field.name};
    }
    public void set${field.name?cap_first}(${field.type} ${field.name}){
        this.${field.name} = ${field.name};
    }
</#list>    
}

Let’s look at the first line:

public class ${classSpecification.name}{

You can see it begins with the static text of a class declaration: public class. The interesting bit is in the middle: ${classSpecification.name}.

When Freemarker processes the template, it accesses the classSpecification object in its model. It calls the getName() method on it.

What about this part of the template?

<#list classSpecification.fieldSpecifications as field>
    private ${field.type} ${field.name};
</#list>

At first, Freemarker calls classSpecification.getFieldSpecifications(). It then iterates over the field specifications.

One last thing. That line is a bit odd:

public ${field.type} get${field.name?cap_first}(){

Let’s say the example field is age: Integer (in YAML). Freemarker translates this to:

public Integer getAge(){

So ?cap_first means: capitalize the first letter, as the YAML file contains age in lower case letters.

Enough about templates. How do you generate the Java source files?

First, you need to configure FreeMarker by creating a Configuration instance. This happens in the constructor of the JavaDataClassGenerator:

To generate source files, the JavaDataClassGenerator iterates over the class specifications, and generates a source file for each:

And that’s it.

Conclusion

I showed you how to build a Java source code generator based on YAML files. I picked YAML because it is easy to process, and thus easy to teach. You can replace it with another format if you like.

You can find the complete code on Github.

To make the code as understandable as possible, I took a few shortcuts:

  • no methods like equals(), hashCode() and toString()
  • no inheritance of data classes
  • generated Java classes are in the default package
  • the output directory is the same as the input directory
  • error handling hasn’t been my focus

A production-ready solution would need to deal with those issues. Also, for data classes, Project Lombok is an alternative without code generation.

So think of this article as a beginning, not an end. Imagine what is possible. A few examples:

  • scaffold JPA entity classes or Spring repositories
  • generate several classes from one specification, based on patterns in your application
  • generate code in different programming languages
  • produce documentation

I currently use this approach to translate natural language requirements
directly to code, for research purposes. What will you do?

If you want to know what I’m hacking on, visit my GitHub project.

You can contact me on Twitter or LinkedIn.

The original version of this article was posted on dev.to