XML to Crystal classes
Crystal's standard library includes an XML module based on libxml2, with XML.parse and XPath queries, but no built-in mapping from XML to typed objects. This XML to Crystal generator gives you the typed side: classes with property declarations inferred from a sample document. You then fill them from the parsed XML, getting compile-time checked models for the rest of your program.
Example: a sitemap
<sitemap>
<url priority="1.0"><loc>https://example.com/</loc><lastmod>2026-05-01</lastmod></url>
<url priority="0.8"><loc>https://example.com/docs</loc><lastmod>2026-04-20</lastmod></url>
</sitemap>
class Sitemap
include JSON::Serializable
property url : Array(Url)
end
class Url
include JSON::Serializable
property loc : String
property lastmod : String
property priority : Float64
end
The priority attribute became Float64; lastmod is a date-only value and stays String.
Filling the classes with Crystal's XML module
The generated classes include JSON::Serializable, which gives them a constructor that reads JSON. For XML, add a small class method that walks the document:
require "xml"
class Url
def self.from_node(node : XML::Node) : Url
json = {
loc: node.xpath_string("string(loc)"),
lastmod: node.xpath_string("string(lastmod)"),
priority: node["priority"].to_f,
}.to_json
Url.from_json(json)
end
end
doc = XML.parse(xml_text)
urls = doc.xpath_nodes("//url").map { |n| Url.from_node(n) }
Attributes are read with node["name"], element text with xpath_string, and all values arrive as strings, so convert with to_f, to_i or a comparison for booleans. Real sitemaps declare a default namespace; with namespaces, pass a namespace map to the XPath calls.
How the XML is mapped
The root element names the top-level class (Sitemap), unless you enter another name. Attributes such as priority and child elements such as <loc> become properties of the same class; if an element has attributes and text, the text becomes a value property. Namespace prefixes are removed and the XML declaration is ignored. Text is typed from its content, so 1.0 becomes Float64 and true a Bool. Keep this mapping in mind when you write the XPath for each property: the class alone does not tell you which properties were attributes.
Things to check
- Repeated elements must appear at least twice in the sample to become
Arrayproperties. - Whole numbers become
Int32; useInt64for large counts or IDs. - Optional elements missing from some records get nilable types such as
String?.
For JSON sources, JSON to Crystal is a direct fit. Prepare the input with the XML Formatter. Generation runs in your browser.
Sitemaps and other namespaced XML
Real sitemaps declare a default namespace, xmlns="http://www.sitemaps.org/schemas/sitemap/0.9". XPath then no longer matches plain //url, which is the usual reason a Crystal program finds zero nodes. Register a prefix for the namespace and use it in the query:
ns = {"s" => "http://www.sitemaps.org/schemas/sitemap/0.9"}
urls = doc.xpath_nodes("//s:url", ns).map { |n| Url.from_node(n) }
The same namespace map must be passed to the xpath_string calls inside from_node, with the prefix in each path, such as string(s:loc). The generated classes need no changes, since the generator already drops namespace prefixes from names.