An open API service indexing awesome lists of open source software.

https://github.com/propensive/xylophone

Working with XML in Scala
https://github.com/propensive/xylophone

scala xml xml-api xml-parser xml-serialization

Last synced: 11 months ago
JSON representation

Working with XML in Scala

Awesome Lists containing this project

README

          

[GitHub Workflow](https://github.com/propensive/xylophone/actions)
[](https://discord.com/invite/MBUrkTgMnA)

# Xylophone

__Typesafe XML for Scala__

_Xylophone_ is an XML library for Scala that takes advantage of many features of the language to
provide intuitive syntax for manipulating XML, as well as better typesafety and static checks.

## Features

- parse and represent XML in Scala
- statically check XML in `x""` interpolators
- substitute standard and custom types (provided by typeclasses) into XML
- automatically derive typeclasses to convert case classes and product types to and from XML
- safe dynamic interface for accessing nested fields

## Availability

## Getting Started

### Parsing

A `Text` value containing XML may be parsed with,
```scala
Xml.parse(text)
```
which will return an instance of `Xml`, or throw an `XmlParseError` if the XML is not well-formed.

### XML Literals

`Xml` values may also be constructed using the `x""` interpolator. These will be checked for well-formedness
at compiletime: all syntax must be valid, special characters escaped, and all tags must be closed and nested
correctly.

```scala
val book = x"H. G. WellsWar of the Worlds"
```

### XML AST Representation

An `Xml` value is a general type representing XML in three different forms, subtypes of `Xml`:
- `XmlDoc` is a complete XML document, and includes the `

H. G. Wells
The War of the Worlds


Virginia Woolf
Mrs. Dalloway

```
as an instance of `XmlNode`, `library`, we can access the two book nodes with `library.book`, as an `XmlFragment`.
Subsequently calling, `library.book.title` would return a new `XmlFragment` consisting of the titles of _both_ books,
specifically,
```xml
The War of the Worlds
Mrs. Dalloway
```

Given an `XmlFragment`, the *nth* node in the sequence may be accessed by applying the node index, for example,
```scala
library.book.title(1)
```
would return,
```xml
Mrs. Dalloway
```
as an `XmlNode`.

The same node could alternatively be accessed with `library.book(1).title(0)`. Note how `0` is applied to the
`title` now (instead of `1`) since we want the first (`0`) `title` element of the second (`1`) 'book' element.
The application of `(0)` returns a single `XmlNode` instance rather than an `XmlFragment` instance.

The `apply` method of `XmlFragment` has a default value of `0` for convenience in the very common case where the
first node is required, e.g. `library.book().title()`

An `XmlFragment` of all the elements inside an `XmlNode` can always be obtained by calling `*` on the `XmlNode`
value. For example, `library.book().*` would return an `XmlFragment` of,
```xml
The War of the Worlds
H. G. Wells
```

### Extracting typed values

An `Xml` value is _dynamic_ in the sense that it could represent a single string value or deeply-nested structured
data. Usually we want to convert `Xml` values to other Scala types in order to use them. This can be achieved by
calling `as[T]` on the value, for an appropriate choice of `T`.

For example, the `XmlNode`,
```scala
val name: XmlNode = x"Virginia Woolf"
```
could be converted to a `Text` with, `name.as[Text]`. Or,
```scala
val age: XmlNode = x"18"
```
can be read as a `Long` with, `age.as[Long]`.

This works for `Text`s and primitive types. But will also work for case classes composed of these types (or
of other nested case classes). For example, given the definition,
```scala
case class Book(title: Text, author: Text)
```
a book from the _library_ example above could be read with:
```scala
library.book().as[Book]
```

The `as` method can also extract collection types (e.g. `Set`, `List` or `Vector`) from an `Xml` value, so
_all_ the books in the library could be accessed with,
```scala
library.book.as[Set[Book]]
```

In general, extraction requires a contextual `XmlReader` typeclass instance for the type to be extracted.
These exist on the `XmlReader` companion object for the basic types, collection types, product types
(e.g. case classes) and coproduct types (e.g. enumerations), but other instances may be provided.

### Writing to XML

Likewise, these same types may be converted to `Xml` by calling the `xml` extension method on them, for
example, given,
```scala
case class Book(title: Text, author: Text)
val book = Book(t"Mrs. Dalloway", t"Virginia Woolf")
```
we could create an `XmlNode` value of,
```xml

Mrs. Dalloway
Virginia Woolf

```
just by calling `book.xml`.

Note that the element labels will be taken from the case class's type name and field names. However, for
nested case classes, a type name will only appear in the XML output for the outermost tag name, since the
field name will be used in these cases.

The type name will also appear in the repeated child nodes of XML produced from a collection type, for
example, writing the `List[Int]`, `List(1, 2, 3)` would produce the XML,
```xml

1
2
3

```

In general, the type name will be used for a node if the context does not suggest a more specific name.

The node nade may be controlled, however, using annotations. The `@xmlLabel` attribute may be applied
to a case class or a case class field to change its name when written or read.

For example, the definition,
```
@xmlLabel(t"book")
case class Book(title: Text, author: Text, @xmlLabel(t"type") kind: Text)
```
would ensure that a `Book` instance is written using the lower-case tag name, `book`, and the `kind`
field would be serialized the name `type` (which cannot be used so easily in Scala, as it's a keyword).

### Serialization

XML usually needs to be serialized to a string. Xylophone provides a `show` method that will serialize
an `Xml` value to a `Text` value using a contextual `XmlPrinter`, of which two are available by default:
one which omits all unnecessary whitespace, and one which "pretty prints" the XML with indentation for
nesting.

## Status

Xylophone is classified as __fledgling__. For reference, Soundness projects are
categorized into one of the following five stability levels:

- _embryonic_: for experimental or demonstrative purposes only, without any guarantees of longevity
- _fledgling_: of proven utility, seeking contributions, but liable to significant redesigns
- _maturescent_: major design decisions broady settled, seeking probatory adoption and refinement
- _dependable_: production-ready, subject to controlled ongoing maintenance and enhancement; tagged as version `1.0.0` or later
- _adamantine_: proven, reliable and production-ready, with no further breaking changes ever anticipated

Projects at any stability level, even _embryonic_ projects, can still be used,
as long as caution is taken to avoid a mismatch between the project's stability
level and the required stability and maintainability of your own project.

Xylophone is designed to be _small_. Its entire source code currently consists
of 717 lines of code.

## Building

Xylophone will ultimately be built by Fury, when it is published. In the
meantime, two possibilities are offered, however they are acknowledged to be
fragile, inadequately tested, and unsuitable for anything more than
experimentation. They are provided only for the necessity of providing _some_
answer to the question, "how can I try Xylophone?".

1. *Copy the sources into your own project*

Read the `fury` file in the repository root to understand Xylophone's build
structure, dependencies and source location; the file format should be short
and quite intuitive. Copy the sources into a source directory in your own
project, then repeat (recursively) for each of the dependencies.

The sources are compiled against the latest nightly release of Scala 3.
There should be no problem to compile the project together with all of its
dependencies in a single compilation.

2. *Build with [Wrath](https://github.com/propensive/wrath/)*

Wrath is a bootstrapping script for building Xylophone and other projects in
the absence of a fully-featured build tool. It is designed to read the `fury`
file in the project directory, and produce a collection of JAR files which can
be added to a classpath, by compiling the project and all of its dependencies,
including the Scala compiler itself.

Download the latest version of
[`wrath`](https://github.com/propensive/wrath/releases/latest), make it
executable, and add it to your path, for example by copying it to
`/usr/local/bin/`.

Clone this repository inside an empty directory, so that the build can
safely make clones of repositories it depends on as _peers_ of `xylophone`.
Run `wrath -F` in the repository root. This will download and compile the
latest version of Scala, as well as all of Xylophone's dependencies.

If the build was successful, the compiled JAR files can be found in the
`.wrath/dist` directory.

## Contributing

Contributors to Xylophone are welcome and encouraged. New contributors may like
to look for issues marked
[beginner](https://github.com/propensive/xylophone/labels/beginner).

We suggest that all contributors read the [Contributing
Guide](/contributing.md) to make the process of contributing to Xylophone
easier.

Please __do not__ contact project maintainers privately with questions unless
there is a good reason to keep them private. While it can be tempting to
repsond to such questions, private answers cannot be shared with a wider
audience, and it can result in duplication of effort.

## Author

Xylophone was designed and developed by Jon Pretty, and commercial support and
training on all aspects of Scala 3 is available from [Propensive
OÜ](https://propensive.com/).

## Name

A _xylophone_ is a musical instrument made from wood ("xylo-") or trees, and it provides a representation of XML trees. "Xylophone" and "XML" begin with the same infrequently-used letter.

In general, Soundness project names are always chosen with some rationale,
however it is usually frivolous. Each name is chosen for more for its
_uniqueness_ and _intrigue_ than its concision or catchiness, and there is no
bias towards names with positive or "nice" meanings—since many of the libraries
perform some quite unpleasant tasks.

Names should be English words, though many are obscure or archaic, and it
should be noted how willingly English adopts foreign words. Names are generally
of Greek or Latin origin, and have often arrived in English via a romance
language.

## Logo

The logo shows two angle brackets (or chevrons), representing the most significant symbols in XML, placed next to each other to look like a capital X.

## License

Xylophone is copyright © 2025 Jon Pretty & Propensive OÜ, and
is made available under the [Apache 2.0 License](/license.md).