XM

Beautiful Elixir DSL for building XML documents, backed by Saxy for escaping and encoding.

import XM
document do
urlset xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9" do
for page <- pages do
url do
loc site_url <> page.path
lastmod page.date
end
end
end
end

XM is intentionally tiny: local calls become XML elements, keyword arguments become attributes, and normal Elixir expressions still work.

Features

Installation

def deps do
[
{:xm, "~> 0.1.0"}
]
end

Examples

Sitemap

import XM
pages = [
%{path: "/", date: ~D[2026-06-25]},
%{path: "/about/", date: ~D[2026-06-25]}
]
xml =
document do
urlset xmlns: "http://www.sitemaps.org/schemas/sitemap/0.9" do
for page <- pages do
url do
loc "https://example.com" <> page.path
lastmod page.date
end
end
end
end

Atom entry with CDATA

import XM
document do
entry do
title "Hello"
content type: "html" do
cdata "<p>Hello from XML</p>"
end
end
end

Namespaces and schema declarations

schema do ... end is document metadata, not an XML element. XM injects namespace declarations into the document root and renders XSD locations as xsi:schemaLocation.

import XM
xml =
document do
schema do
default "http://www.sitemaps.org/schemas/sitemap/0.9",
location: "priv/schemas/sitemap.xsd"
ns :image, "http://www.google.com/schemas/sitemap-image/1.1",
location: "priv/schemas/sitemap-image.xsd"
end
urlset do
url do
loc "https://example.com/"
image.image do
image.loc "https://example.com/image.jpg"
end
end
end
end

Namespaced or dynamic tags

import XM
tree do
tag qname(:media, :thumbnail), [xmlns(:media, "https://example.com/media"), url: "https://example.com/image.png"]
end

Iodata rendering

document do ... end is the convenience API for producing a binary XML document. For iodata, build nodes with tree do ... end and render explicitly:

import XM
iodata =
tree do
feed do
title "Hello"
end
end
|> XM.render_iodata()
IO.iodata_to_binary(iodata)

This mirrors common Elixir conventions: keep binary and iodata rendering as separate functions instead of overloading a single render/2 option.

XSD validation

Use XM.validate!/2 explicitly:

XM.validate!(xml)
XM.validate!(xml, schema: "priv/schemas/sitemap.xsd")
XM.validate!(xml, schemas: ["priv/schemas/sitemap.xsd", "priv/schemas/sitemap-image.xsd"])

Without explicit :schema/:schemas, XM reads schema locations from the parsed root element's xsi:schemaLocation or xsi:noNamespaceSchemaLocation attributes.

To validate every document do ... end, enable XM's global compile-time configuration before modules using document/2 are compiled:

config :xm, validate: true

The option is captured when the document do ... end macro expands. It is intentionally global; there is no per-document validate: option. If validation is enabled and the document does not declare schema locations, XM raises %XM.Error{reason: :missing_schema}.

License

MIT © 2026 Danila Poyarkov