All software

J%

A Java extension that embeds domain-specific languages as first-class types, checked by the compiler instead of hidden inside strings.

2009–2015JavaJ%

J% (j-mod) extends Java with external types: a declaration that looks like a class but whose body is DSL text rather than Java statements. An SQL query, a regular expression or a JSON document written this way is parsed and checked when the program is compiled, so the errors that normally wait until runtime — a syntax slip, a column that does not exist, a value of the wrong type — become compilation errors instead.

The idea it argues against is the string literal. Embedding one language inside another by quoting it hides the guest language from every tool that reads the host: the compiler cannot check it, the IDE cannot complete it, and the type system cannot connect the values spliced into it with the code around it. J% keeps the guest language visible.

The language

An external type declares its DSL by the base type it extends. Everything between the braces is guest-language text, read by the module rather than by javac:

package examples.simpleregex;

import org.jmod.dsl.regex.Regex;
import org.jmod.dsl.regex.RegexConfiguration;

public external IpAddress extends Regex<RegexConfiguration> {
([0-9]{1,3}\\.){3}[0-9]{1,3}
}

After compilation that is an ordinary Java class, with no trace of the extension left:

IpAddress ip = new IpAddress();
ip.matches("127.0.0.1");

Host values are spliced into the body with #[name]<JavaType>, where the type defaults to String. A splice is not string interpolation: the name becomes a constructor parameter of that Java type, and each module decides what to do with it — the SQL module turns it into a bound JDBC parameter, the JSON module into an encoded value. That is the whole point of the notation. The guest text and the host values stay separate all the way through compilation, so they cannot be concatenated into each other by accident.

The type argument names a configuration: a plain Java class of constants that switches the deeper checks on. Omit it and the module's default applies, which checks the syntax of the body and nothing more.

public class SimpleConf extends SQLConfiguration {
    public boolean SQLMOD_NS_AWARE = true;
    public String SQLMOD_NS_URI = "file://./schema.sql";
}

Configurations are ordinary classes, read at compile time and inheritable: extend one and override a single field. Keep the values plain literals, and let relative file://./… paths resolve against the configuration's own directory.

The modules

A DSL is a compiler module: it validates the body, then generates the Java class that replaces it. Four ship with the prototype, and each has a page of its own — how you write one, what the compiler checks for you, and where the module stops:

The modules share no code beyond the framework: adding a fifth means a Module subclass, a runtime base type, a template and one line in the registry. GetSet is the shortest example of all four.

What the compiler catches

The claim is worth only as much as the errors it produces, so here are two, as the compiler prints them. A malformed pattern, which would otherwise throw on first use:

BadRegex.jmod: invalid regular expression: Unclosed character class near index 21

And a host value whose Java type does not fit the column it is bound to — the check that most justifies the exercise, since nothing about the Java alone is wrong:

select nickname from users where usr_id = #[id]<String>

BadColumn.jmod: type incompatibility: #[id]<String> is not compatible
with column usr_id (INTEGER)

Unknown tables and columns, invalid SQL and JSON that does not match its schema come back the same way. In every case the build stops — these are compilation failures, not warnings.

Using it

A JDK 17 or newer is the only requirement; the repository carries a Maven bootstrap that fetches Maven itself, so a clone builds with nothing else installed.

./mvnw package
java -jar target/jmod-0.2.0.jar -i examples/simpleregex -o out
java -cp out:target/jmod-0.2.0.jar examples.simpleregex.Main

Point -i at a directory and everything in it is compiled: .jmod files become Java classes, ordinary .java files are carried through unchanged, and javac runs over the result unless -n stops it. -o chooses the output directory, -l lists the modules, and --help has the rest.

It is a source-to-source compiler: no javac plugin, no bytecode rewriting, and the classes it generates extend runtime types that ship in the same jar — which is why that jar is on the classpath when the compiled program runs.

Highlighting J% in your own pages

No syntax highlighter ships a grammar for J%, and the stock ones get the notation that matters exactly wrong: every one of them reads the # of a splice as the start of a line comment and greys out the rest of the line. The snippets on this site are coloured by the small highlight.js (external link, opens in a new tab) grammar below, which is free to copy.

const IDENT = /[A-Za-z_$][A-Za-z0-9_$]*/;

export default function jmod(hljs) {
  // #[name]<JavaType> — the one extension J% makes to a guest language.
  const SPLICE = {
    match: [/#\[[A-Za-z_$][A-Za-z0-9_$]*\]/, /(?:<[^<>\n]{0,80}>)?/],
    scope: { 1: "template-variable", 2: "type" },
    relevance: 10
  };

  // The body: the declaration's brace through the matching one, left as one
  // span — which guest language it holds is the module's business, not ours.
  const BODY = {
    scope: "string",
    begin: /\{[ \t]*$/,
    end: /^[ \t]*\}/,
    contains: [SPLICE],
    relevance: 0
  };

  return {
    name: "J%",
    aliases: ["jmod"],
    keywords: { keyword: "package import public external extends" },
    contains: [
      hljs.C_LINE_COMMENT_MODE,
      hljs.C_BLOCK_COMMENT_MODE,
      { match: [/\bexternal\b/, /\s+/, IDENT],
        scope: { 1: "keyword", 3: "title.class" }, relevance: 10 },
      { match: [/\bextends\b/, /\s+/, /[A-Za-z_$][A-Za-z0-9_$.]*/, /(?:<[^<>\n]*>)?/],
        scope: { 1: "keyword", 3: "title.class", 4: "type" }, relevance: 5 },
      BODY,
      SPLICE
    ]
  };
}

Register it under a name of your choosing and highlight as usual:

import hljs from "highlight.js/lib/core";
import jmod from "./jmod-highlight.js";

hljs.registerLanguage("jmod", jmod);
document.querySelectorAll("pre code.language-jmod")
  .forEach((el) => hljs.highlightElement(el));

Two things worth knowing if you adapt it. Label your J% blocks explicitly rather than leaving them to highlightAuto: a .jmod file is mostly guest-language text, so it scores badly against real grammars, and the reliable signal is its shape — an external … extends … line, which no other language has. And highlight.js honours a scope object only when match is an array of regular expressions; hand it a single regex with capture groups and it compiles without complaint and colours nothing at all.

Publications

Vassilios Karakoidas and Diomidis Spinellis, J%: Integrating Domain Specific Languages with Java, 13th Panhellenic Conference on Informatics (PCI 2009), pp. 109–113.

Vassilios Karakoidas, Dimitris Mitropoulos, Panos Louridas and Diomidis Spinellis, A type-safe embedding of SQL into Java using the extensible compiler framework J% (external link, opens in a new tab), Computer Languages, Systems & Structures, 2015 — on this site.

Vassilios Karakoidas, Integrating Domain-specific Languages to General-purpose Languages, PhD thesis, Athens University of Economics and Business, 2015. The thesis is the full account of the design; the papers cover the SQL module and the original announcement.

Vassilios Karakoidas, Design Concepts of the J% Programming Language, revised edition, 2026. The thesis's design chapter brought up to date against this prototype: the body-delimiting and splice rules stated precisely, the obligations a module owes the compiler written down as a contract with a safety theorem, and an honest account of what checking SQL against a live database can and cannot promise.

Source

The compiler here is a prototype rebuilt around the thesis design, not the original research artefact. J% once had its own site at jmod-lang.org; the domain is gone, and this page replaces it.