The Java library
dev.stxt:stxt-core is the
STXT parser for Java, published on Maven Central. It implements the five
specifications —syntax, canonical tree, schemas, templates and resolution— and
reports the same error codes as the TypeScript library, the
Python one and the command line. It is a library: it
ships no CLI; the command of the ecosystem is stxt.
This page is the usage guide: parsing, walking and building trees, validating against schemas and templates, resolving which grammar applies to a document, obtaining its canonical JSON and writing STXT back out. It has the same structure as the TypeScript library guide, so you can move from one to the other without surprises; where the Java API differs, it says so.
Installation
Requires Java 17 or later. It has no runtime dependencies and under JPMS it
is an automatic module named dev.stxt.
<dependency>
<groupId>dev.stxt</groupId>
<artifactId>stxt-core</artifactId>
<version>0.7.2</version>
</dependency>
Or with Gradle:
implementation 'dev.stxt:stxt-core:0.7.2'
The javadoc of every version is on
javadoc.io, and the artifact's
README is its front page on Central: its examples are compiled and run against
the real parser.
Parsing
Parser has two entry points. parseResult(text) collects every error and
also returns the nodes it managed to build; parse(text) throws a
ParseException on the first one and returns List<Node> when there is none.
Both have a file-based counterpart: parseResultFile(File) and parseFile(File).
The STXT facade gives ready-made parsers: STXT.rawParser() is a parser with
no validation (syntax only) and STXT.parser(loader) one with schema and
template validation registered, as shown below.
import java.util.List;
import dev.stxt.Node;
import dev.stxt.ParseResult;
import dev.stxt.Parser;
import dev.stxt.exceptions.ParseException;
import dev.stxt.runtime.STXT;
Parser parser = STXT.rawParser();
ParseResult result = parser.parseResult(text);
if (result.hasErrors()) {
for (ParseException error : result.getErrors()) {
System.err.printf("line %d [%s]: %s%n", error.getLine(), error.getCode(), error.getMessage());
}
}
List<Node> roots = result.getNodes(); // the root nodes, in order; there may be several
// The "throw on first error" form
try {
List<Node> nodes = parser.parse(text);
} catch (ParseException e) {
System.err.println(e.getLine() + " " + e.getCode() + " " + e.getMessage());
}
Every error is a ParseException with getLine() (the line of the document,
starting at 1), getCode() (stable, upper-case: INVALID_LINE,
MIXED_INDENTATION, INDENTATION_LEVEL_NOT_VALID…) and getMessage().
Grammar errors are ValidationException, a subclass with the same fields, so a
single loop walks both and instanceof tells the severity apart. Every exception
is unchecked and extends STXTException (see Errors). A document with a syntax
error may still return nodes: the parser recovers and carries on, so that an
editor can underline everything that is wrong at once.
The tree
Node is a sealed class with exactly two forms, and each one owns only what
is really its own:
| Class | Syntax | What it owns |
|---|---|---|
InlineNode |
Name: value |
getValue()/setValue(), getChildren(), getChild(name), getChildren(name), addChild(), removeChild(), addInlineNode(), addTextNode() |
TextNode |
Name >> |
getTextLines(), setText(), setTextLines(), addTextLine(), clearText() |
What they share lives in Node: getName() and getCanonicalName() (the canonical
name of STXT-SPEC: lower-case, NFC, unified separators), getDeclaredNamespace()
(what the node writes between parentheses, or "") and getNamespace() (the
effective one, inherited through the chain of parents), getLine(),
getLevel() (derived from the depth), getParent() (always an InlineNode, or
null at the root), detach() and getText() —the value of an inline node or the
joined lines of a block—. Walking a tree means asking for the form with
instanceof, using Java 17 pattern matching, the same way the canonical tree of
STXT-TREE-SPEC has children only for inline nodes and lines only for blocks.
Child lookups go by canonical name (getChild("Título") and
getChild("titulo") find the same node), getChild returns null when there is
none and throws AMBIGUOUS_CHILD when there is more than one —repeated children
are what getChildren(name) is for—; both accept a second argument with the
namespace.
With the document of the tutorial, a book record:
Book (com.acme.book):
Title: Arquitectura de software moderna
Authors:
Author: María Pérez
Author: Juan García
ISBN: 978-84-123456-7-8
Published: 2025-10-01
Chapter: Introducción
Content >>
Conceptos básicos y objetivos del libro.
import dev.stxt.InlineNode;
import dev.stxt.Node;
import dev.stxt.TextNode;
import dev.stxt.runtime.STXT;
Node book = STXT.rawParser().parseResult(text).getNodes().get(0);
book.getName(); // "Book"
book.getCanonicalName(); // "book"
book.getNamespace(); // "com.acme.book"
book.getLine(); // 1
if (book instanceof InlineNode inline) {
inline.getChild("Title").getText(); // "Arquitectura de software moderna"
inline.getChild("title").getName(); // "Title": lookups go by canonical name
inline.getChild("Publisher"); // null: not there
InlineNode authors = (InlineNode) inline.getChild("Authors");
authors.getChildren("Author").stream().map(Node::getText).toList(); // [María Pérez, Juan García]
authors.getDeclaredNamespace(); // "": it declares none…
authors.getNamespace(); // "com.acme.book": …it inherits it
InlineNode chapter = (InlineNode) inline.getChild("Chapter");
if (chapter.getChild("Content") instanceof TextNode content) {
content.getTextLines(); // [Conceptos básicos y objetivos del libro.]
content.getLevel(); // 2
content.getParent() == chapter; // true
}
}
// A generic walk
static void walk(Node node, int depth) {
System.out.println(" ".repeat(depth) + node.getName());
if (node instanceof InlineNode inline)
for (Node child : inline.getChildren()) walk(child, depth + 1);
}
Building and editing
Trees are mutable and keep their own integrity: every node knows its parent,
addChild links both ends and refuses a node that already has one
(NODE_ALREADY_ATTACHED) or that is an ancestor (NODE_CYCLE); removeChild and
detach() undo it. Levels are derived from the chain of parents, and the line is
only set by the parser. In the factories with two Strings, the second one is
always the content (value or text); the namespace only appears in the
three-argument form. To insert at a position, addChild(index, node).
import dev.stxt.InlineNode;
import dev.stxt.TextNode;
InlineNode email = new InlineNode("Email", "com.example.mail", "Weekly report");
email.addInlineNode("From", "Ana García <[email protected]>");
InlineNode to = email.addInlineNode("To");
to.addInlineNode("Address", "[email protected]");
TextNode body = email.addTextNode("Body", "Hi Bob,\n\nSee attached.");
body.getParent() == email; // true
body.getLevel(); // 1
to.getNamespace(); // "com.example.mail", inherited
// Reorder: "To" first
to.detach();
email.addChild(0, to);
// Edit
email.setNamespace("com.example.docs"); // the whole inheriting subtree follows
body.setText("Hi Bob,\n\nSee the new attachment.");
Validating against a schema or a template
Grammars are STXT documents in the reserved namespaces @stxt.schema and
@stxt.template; the template is the short authoring form and compiles to a
schema when loaded. In Java, a ResourcesLoader says where they come from:
ResourcesLoaderDirectory looks them up on disk with the layout
<dir>/@stxt.schema/<namespace>.stxt and <dir>/@stxt.template/<namespace>.stxt
—exactly the one stxt install leaves, so a project's .stxt/ works as is—, and
STXT.parser(loader) returns a parser that resolves both kinds, validates each
grammar against its meta-schema when loading it, caches it and validates every
node with a namespace as it is closed: underneath it registers a
SchemaValidator wrapped in a ConditionalValidator, which lets the nodes
without a namespace through, because that is the rule of the language —a document
without a namespace is not wrong, it just cannot be validated—.
Template (@stxt.template): com.acme.book
Structure >>
Book:
Title: (1)
Authors: (1)
Author: (+)
ISBN: (1)
Publisher: (?)
Published: (?) DATE
Summary: (?) TEXT
Chapter: (+)
Content: (?) TEXT
Description >>
Book: Template for publisher book records
import java.io.File;
import dev.stxt.ParseResult;
import dev.stxt.Parser;
import dev.stxt.exceptions.ParseException;
import dev.stxt.exceptions.ValidationException;
import dev.stxt.resources.ResourcesLoader;
import dev.stxt.resources.ResourcesLoaderDirectory;
import dev.stxt.runtime.STXT;
ResourcesLoader loader = new ResourcesLoaderDirectory(new File("/home/ana/libros/.stxt"));
Parser parser = STXT.parser(loader);
ParseResult result = parser.parseResult(documentText);
for (ParseException error : result.getErrors()) {
String kind = (error instanceof ValidationException) ? "schema" : "syntax";
System.out.printf("%s line %d [%s]: %s%n", kind, error.getLine(), error.getCode(), error.getMessage());
}
With a book that lacks its ISBN and whose date is not YYYY-MM-DD, the loop
prints the same codes as stxt validate:
schema line 5 [INVALID_VALUE]: Error at line: 5, Published: Invalid date (1 de octubre de 2025)
schema line 1 [INVALID_NUMBER]: Error at line: 1, 0 nodes of 'com.acme.book:isbn' and min is 1
ResourcesLoader is a single-method interface, retrieve(namespace, resource)
—where namespace is @stxt.schema or @stxt.template and resource the
namespace being looked up—, so a grammar in memory, or taken from the
classpath, a database or a URL, is a lambda that returns its text, or throws
ResourceNotFoundException when it does not have it:
import dev.stxt.exceptions.ResourceNotFoundException;
ResourcesLoader loader = (namespace, resource) -> {
if (namespace.equals("@stxt.template") && resource.equals("com.acme.book")) return templateText;
throw new ResourceNotFoundException(namespace, resource);
};
Parser parser = STXT.parser(loader);
Rules worth knowing:
- A cardinality error is reported on the line of the parent, which is the one
with zero
ISBN. - If the document uses a namespace the loader does not know, each node produces a
SCHEMA_NOT_FOUND; the provider never throws for a missing namespace (getSchema()returnsnull). - A grammar that does not validate against its meta-schema is reported as a
finding of the document (for instance
TYPE_NOT_VALID), not as an exception. - Nodes without a namespace are not validated (thanks to
ConditionalValidator); a document that is itself a definition is always validated against its meta-schema. If instead of the facade you register aSchemaValidatorby hand, it validates every node: wrap it in theConditionalValidatoryourself to get the same behaviour as the CLI and the extension.
import dev.stxt.runtime.ConditionalValidator;
import dev.stxt.schema.SchemaValidator;
Parser parser = new Parser();
parser.registerValidator(new ConditionalValidator(new SchemaValidator(STXT.schemaProvider(loader))));
- The available value types are those of STXT-SCHEMA-SPEC:
INLINE,BLOCK,TEXT,MARKDOWN,BOOLEAN,INTEGER,NATURAL,NUMBER,DATE,TIME,TIMESTAMP,UUID,EMAIL,URL,HEXADECIMAL,BINARY,BASE64,GROUPandENUM. To add your own, implementdev.stxt.schema.Typeand register it inTypeRegistry.
The compiled schema can be inspected: STXT.schemaProvider(loader) is the
SchemaProvider the facade uses, and getSchema(ns) returns a Schema with
getNamespace(), getNodes() and getNodeDefinition(name); each
NodeDefinition has getType(), getChildren() (a map of ChildDefinition by
qualified name namespace:name, with getMin() / getMax()), getValues() for
an ENUM and getDescription().
Resolution: which grammar applies to a document
ResourcesLoaderDirectory looks in one directory. Resolution answers the
full question: given this document, which definitions apply to it?
DiscoveryResolver implements STXT-DISCOVERY-SPEC —the
.stxt/ directories of the document and of all its ancestors, then ~/.stxt and
/etc/stxt, per-namespace precedence, STXT_PATH replacing the chain—, just like
the CLI and the extension, so all three agree by construction.
Unlike the TypeScript port, which has to run on several hosts, here disk access is
direct through java.nio.file; the only injectable piece is the
DiscoveryEnvironment (STXT_PATH, user and system directories), with
SystemDiscoveryEnvironment as the real implementation —the one the no-argument
constructor uses— and the option of passing your own in a test. The chain is per
document: pass the directory it lives in (null for standard input or an unsaved
buffer, which starts the chain at the user level). Since DiscoveryResult
implements SchemaProvider, resolving and validating are two steps:
import java.nio.file.Path;
import dev.stxt.Parser;
import dev.stxt.discovery.DiscoveryDefinition;
import dev.stxt.discovery.DiscoveryError;
import dev.stxt.discovery.DiscoveryResolver;
import dev.stxt.discovery.DiscoveryResult;
import dev.stxt.runtime.ConditionalValidator;
import dev.stxt.schema.SchemaValidator;
DiscoveryResolver resolver = new DiscoveryResolver();
DiscoveryResult discovery = resolver.resolve(Path.of("/home/ana/libros/docs"));
discovery.getChain(); // [/home/ana/libros/.stxt] (every ancestor, nearest first)
// Resolution errors are collected, never thrown: report them and carry on
for (DiscoveryError error : discovery.getErrors()) {
System.err.printf("[%s] %s: %s%n", error.getCode(), error.getFile(), error.getMessage());
}
Parser parser = new Parser();
parser.registerValidator(new ConditionalValidator(new SchemaValidator(discovery)));
ParseResult result = parser.parseResult(documentText);
DiscoveryResult also tells you where each grammar came from, which is what an
editor needs for "go to definition" or a diagnostic that explains itself:
DiscoveryDefinition definition = discovery.getDefinition("com.acme.book");
definition.getFile(); // /home/ana/libros/.stxt/@stxt.template/com.acme.book.stxt
definition.getLevelDir(); // /home/ana/libros/.stxt (the level that won)
definition.getSchema(); // the compiled Schema
discovery.getActiveDefinitions(); // one per namespace, precedence applied
discovery.getAllSchemas(); // just the schemas of the above
Levels are cached by directory: resolving many documents that share ancestors reads
each .stxt/ once. Call resolver.clearCache() when the definition files may have
changed. The resolution error codes are DISCOVERY_DUPLICATE_NAMESPACE,
DISCOVERY_NOT_A_DEFINITION, DISCOVERY_NOT_PARSEABLE and
DISCOVERY_INVALID_DEFINITION; DiscoveryError is a data class (getCode(),
getFile(), getMessage(), getNamespace()), not an exception, because the spec
wants a bad definition reported without stopping the load of the rest.
The canonical tree as JSON
TreeJson.toCanonicalJson(nodes) —or toCanonicalJson(node) for a single root—
writes the JSON of STXT-TREE-SPEC, the same one stxt describe
emits, with two-space indentation. It is an explicit function that emits only the
normative fields —children for inline nodes and lines for blocks, with no
positions or comments— and depends on no JSON library: it returns the text,
which you then hand to Jackson, Gson or whatever the program uses.
import dev.stxt.runtime.TreeJson;
String json = TreeJson.toCanonicalJson(result.getNodes());
System.out.print(json);
Writing STXT
NodeWriter makes the trip back: it serialises a node, or a list of documents, to
STXT text in canonical form, with IndentStyle.TABS (default) or
IndentStyle.SPACES_4. It writes the namespace where the node declares it, not
where it differs from the parent: faithful to the source. Writing a tree out and
parsing it back yields the same tree, in both styles. Since it starts from the
logical tree, a text that goes through parse and NodeWriter comes out without
comments or blank lines outside blocks —that is what stxt format --clean does—;
the reformatting that keeps them is the job of the CLI and the extension, not of
the library.
import dev.stxt.runtime.NodeWriter;
import dev.stxt.runtime.NodeWriter.IndentStyle;
String one = NodeWriter.toSTXT(email); // one node, with tabs
String all = NodeWriter.toSTXT(result.getNodes(), IndentStyle.SPACES_4); // a whole document
The email built above, written with toSTXT:
Email (com.example.mail): Weekly report
To:
Address: [email protected]
From: Ana García <[email protected]>
Body >>
Hi Bob,
See attached.
Observing and validating while streaming
The parser knows nothing about schemas: validation is a decoupled layer plugged in
through two hooks. Observer receives streaming callbacks while the document
is parsed —onCreate(node) when a node is opened, already with its parent,
effective namespace and level, and onFinish(node) when it is closed—, and
Validator runs on each node as it is closed and returns a list of
ValidationException (it does not throw), so a document is validated while it is
read, without waiting for the end. SchemaValidator is just a built-in
Validator; Validator is a functional interface, so a lambda will do.
import java.util.List;
import dev.stxt.Node;
import dev.stxt.Parser;
import dev.stxt.exceptions.ValidationException;
import dev.stxt.processors.Observer;
Parser parser = new Parser();
parser.registerObserver(new Observer() {
@Override public void onCreate(Node node) { System.out.println("open " + node.getQualifiedName()); }
@Override public void onFinish(Node node) { System.out.println("close " + node.getQualifiedName()); }
});
parser.registerValidator(node -> List.<ValidationException>of()); // a validator that never complains
parser.parseResult(text);
Errors
Every exception is unchecked and extends dev.stxt.exceptions.STXTException, with
an upper-case code in getCode():
| Exception | When |
|---|---|
ParseException |
The syntax is wrong; adds getLine() |
ValidationException |
The document breaks its grammar (type, cardinality, undeclared child); a subclass of ParseException |
SchemaException |
The schema or template itself is malformed |
ResourceNotFoundException |
A ResourcesLoader has no such resource (providers turn it into a SCHEMA_NOT_FOUND finding) |
STXTIOException |
Reading a file failed |
STXTException (base) |
Tree integrity (NODE_ALREADY_ATTACHED, NODE_CYCLE), an ambiguous lookup (AMBIGUOUS_CHILD) and other runtime failures |
The ones coming out of parseResult are not thrown: they are collected in
getErrors() as ParseException (syntax) or ValidationException (grammar).
Tree-integrity ones are thrown, because they are program errors, not document
errors.
The API surface
| Package | What is there |
|---|---|
dev.stxt |
Parser, ParseResult, Node, InlineNode, TextNode, Constants |
dev.stxt.exceptions |
STXTException, ParseException, ValidationException, SchemaException, ResourceNotFoundException, STXTIOException |
dev.stxt.processors |
Observer, Validator |
dev.stxt.schema |
Schema, NodeDefinition, ChildDefinition, SchemaProvider, SchemaValidator, SchemaProviderResources, SchemaProviderCache, SchemaProviderMeta, SchemaParser, Type, TypeRegistry, and the types in dev.stxt.schema.type |
dev.stxt.template |
TemplateParser, TemplateSchemaProvider, MetaTemplateSchemaProvider |
dev.stxt.resources |
ResourcesLoader, ResourcesLoaderDirectory |
dev.stxt.runtime |
STXT (the facade), ConditionalValidator, NodeWriter and IndentStyle, TreeJson |
dev.stxt.discovery |
DiscoveryResolver, DiscoveryResult, DiscoveryDefinition, DiscoveryLevel, DiscoveryError, DiscoveryEnvironment, SystemDiscoveryEnvironment |
Until 1.0, a minor release can change the in-memory API (0.7.0 redid the node
model), but never the language or the canonical tree: a document that parses
and validates today parses and validates the same tomorrow. The release notes of
each version are in the CHANGELOG.md of the
repository, which is also where its
errors are reported.