A DSL parser and code generator
  • Java 94.8%
  • ANTLR 3.4%
  • Shell 1.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-11-08 10:49:22 +01:00
pannkaka-assembly Prepare for next development iteration 1.5.0-SNAPSHOT 2026-11-03 08:15:31 +01:00
pannkaka-cli Prepare for next development iteration 1.5.0-SNAPSHOT 2026-11-03 08:15:31 +01:00
pannkaka-generator Prepare for next development iteration 1.5.0-SNAPSHOT 2026-11-03 08:15:31 +01:00
pannkaka-maven-plugin Prepare for next development iteration 1.5.0-SNAPSHOT 2026-11-03 08:15:31 +01:00
pannkaka-parser Prepare for next development iteration 1.5.0-SNAPSHOT 2026-11-03 08:15:31 +01:00
.gitignore Ignore genreated pannkaka files 2025-11-13 16:51:23 +01:00
Jenkinsfile Publish the command line tool to Forgejo packages on release 2026-10-26 08:55:06 +01:00
LICENSE Add license header 2025-11-15 21:04:20 +01:00
pom.xml Prepare for next development iteration 1.5.0-SNAPSHOT 2026-11-03 08:15:31 +01:00
publishing.txt Prepare for publishing to central 2025-11-16 12:55:42 +01:00
README.md Add some more documentation 2026-11-08 10:49:22 +01:00

pannkaka

Pannkaka ('pancake') is a tool for generating Java source code from a DSL.

Pannkaka uses the Apache Software Licenese 2.0

The code it generates consists of java Records and De-/Serializers using DataOutputStream and DataInputStream. The generated code only imports JDK packages, no external dependencies.

The use case is to describe your data and messages in the DSL, then generate the code to Marshall those messages to an external format suitable for IO/Network transfer.

Pannkaka is not a generic framework for serialization like protobuf/Kryo or similar. It is meant to be a light-weight alternative for simple use cases. You generate the code, maybe even just one time and then treat it as source code in your own project.

How to install

Pannkaka requires Java 25 or later.

Download the archive from the release page and extract it. There is a bin directory with a launcher script that should work in bash.

How to write a DSL.

Here is an empty sample DSL:

config {}
records {}
messages {}

Config block

The config block is optional. It can be used to set package name and message interface name for the generated code.

    config {
        package: my.example.messages,
        messageInterface: MyMessage
    }

Records block

The records block defines the data structures (Java records) that will be generated.
Each entry becomes a Java record with the same name and fields in the same order.

In the sample DSL:

records {
    Foo(
        int intbar,
        boolean boolbar,
        char charbar,
        float floatbar,
        double doublebar,
        byte bytebar,
        short shortbar,
        long longbar,
        String stringbar
    );
    Bar(String baz, Foo foo);
}

This will generate two records, Foo and Bar. Foo contains primitive and String fields, while Bar shows that records can reference other records (Foo foo).

Records field types

Pannkaka supports the following field types:

  • Simple types:
    • int
    • boolean
    • char
    • float
    • double
    • byte
    • short
    • long
    • String
  • Array of simple types:
    • int[]
    • boolean[]
    • char[]
    • float[]
    • double[]
    • byte[]
    • short[]
    • long[]
    • String[]

A field can also be a reference to another record or an Array of records.

Messages block

The messages block defines the messages that will be sent over the wire.
Each message is also generated as a Java record and typically composes previously defined records.

In the sample DSL:

messages {
    AddFoToBarMessage(Foo foo, Bar bar, boolean fakeFlag);
    RemoveBarFromFooMessage(char fakeChar, Bar bar, Foo foo);
}

Here, AddFoToBarMessage and RemoveBarFromFooMessage are messages that use Foo and Bar as payload types, along with primitive fields. These message definitions will be turned into serializable Java records together with matching de-/serializers.

The generated code

Records and messages

The generated code consists of three classes for each record and message:

  • A record class with the same name as the record/message in the DSL.
  • A serializer class with the same name as the record/message in the DSL, but suffixed with "Serializer".
  • A deserializer class with the same name as the record/message in the DSL, but suffixed with "Deserializer".

SerDe

For each DSL, Pannkaka generates a SerDe (serializer/deserializer) class for your messages.
Its name is derived from the messageInterface value in the config block:

  • If you configure messageInterface: MyMessage
  • The generated SerDe class will be named MyMessageSerDe

This class exposes static methods to write and read messages using DataOutputStream and DataInputStream.

A typical usage from Java code looks like this:

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;

// import your generated types and SerDe class:
// import my.example.messages.*; // e.g. MyMessage, Foo, Bar, MyMessageSerDe

public class Example {
    public static void main(String[] args) throws Exception {
        // Construct a message using generated records
        Foo foo = new Foo(1, true, 'x', 1.0f, 2.0, (byte)3, (short)4, 5L, "hello");
        Bar bar = new Bar("baz", foo);
        MyMessage message = new AddFooToBarMessage(foo, bar, false);
    
        // Serialize
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try (DataOutputStream dos = new DataOutputStream(baos)) {
            MyMessageSerDe.write(message, dos);
        }
        byte[] bytes = baos.toByteArray();

        // Deserialize
        try (DataInputStream dis = new DataInputStream(new ByteArrayInputStream(bytes))) {
            MyMessage roundTripped = MyMessageSerDe.read(dis);

            // Use the deserialized message (pattern match / instanceof / switch, etc.)
            if (roundTripped instanceof AddFooToBarMessage add) {
                System.out.println("Got AddFooToBarMessage with baz = " + add.bar().baz());
            }
        }
    }
}

In summary:

  • Construct your generated message record(s).
  • Use the generated *MessageSerDe.write(message, DataOutputStream) to serialize.
  • Use *MessageSerDe.read(DataInputStream) to get back an instance of your message interface.
  • Dispatch on the concrete message type (switch or instanceof) to handle different messages.

Maven

There is also a maven plugin to generate sources from DSL files. See pannkaka-maven-plugin

AI/LLM Policy

LLMs were used to update the build pipeline (Jenkinsfile).

No code or documentation was generated by LLMs.

Pannkaka internals

This describes the source code of pannkaka itself, not the code it generates.

A Pannkaka DSL file is parsed by a parser generated from ANTLR in the pannkaka-parser module. This is the grammar for pannkaka files: SerDe.g4

The AST from ANTLR is transformed to an internal representation by AntlrToDocumentTransformer.java. The internal model is simply a Java Record class Document.java that holds Config, Records and Messages.

The Document is then used as input to a generator, in the pannkaka-generator module, that writes the actual Java classes corresponding to the DSL, this is done using Java Template Engine by JavaJteGenerator.java

The templates are probably the hardest to read and update, I found that it helps if you generate some sample pannkaka file and inspect the output side by side when looking in the JTE templates.

The other maven modules are:

  • pannkaka-cli - Using picocli it implements the CLI tool.
  • pannkaka-maven-plugin - A maven mojo to run pannkaka from a maven build as an alternative to the CLI.
  • pannkaka-assembly - Is using the maven-assembly-plugin to package the CLI tool and libraries to a zip file for distributing the CLI tool.