XML to Ruby: typed models with dry-struct
Ruby makes it easy to parse XML, with Nokogiri or the bundled REXML, but the result is either a node tree or an untyped hash. This XML to Ruby generator writes Dry::Struct classes from a sample document, so your application works with typed, validated objects. Attributes and child elements both become struct attributes, and repeated elements become Types.Array attributes.
Example: gem metadata
<gem name="rails" version="7.2.0">
<dependency name="activesupport" requirement="= 7.2.0"/>
<dependency name="actionpack" requirement="= 7.2.0"/>
<licenses><license>MIT</license></licenses>
</gem>
class Dependency < Dry::Struct
attribute :dependency_name, Types::String
attribute :requirement, Types::String
end
class Licenses < Dry::Struct
attribute :license, Types::String
end
class Gem < Dry::Struct
attribute :dependency, Types.Array(Dependency)
attribute :licenses, Licenses
attribute :gem_name, Types::String
attribute :version, Types::String
end
The name attributes were renamed dependency_name and gem_name to avoid clashing with methods that already exist on the struct classes. Also note the version 7.2.0 stays a string, while <licenses> with a single <license> became an object rather than an array. Add a second license to the sample if a gem can have several.
Getting the XML into the structs
The generated from_dynamic! methods expect a hash with attributes as plain keys. Rails' ActiveSupport produces a close match:
hash = Hash.from_xml(xml_text)["gem"] # attributes become keys
gem = Gem.from_dynamic!(hash)
puts gem.dependency.map(&:requirement)
Hash.from_xml merges attributes and child elements into one hash and turns repeated elements into arrays, much like this tool's mapping. With Nokogiri, build the hash yourself: node["name"] reads an attribute and node.at("license").text reads child text. Remember that all XML values are strings; strict dry-types reject a string where an integer is expected, so convert values or use Types::Coercible variants.
Nokogiri or REXML?
REXML is pure Ruby and became a bundled gem in Ruby 3.0, so it may need a Gemfile entry on newer Rubies; it is fine for small documents. Nokogiri wraps libxml2, is much faster, and supports XPath and CSS selectors, which makes it the usual choice for large files or scraping.
Notes
- Namespace prefixes are removed from attribute names.
- Add
dry-structanddry-typesto your Gemfile. - For untrusted XML, Nokogiri's default parse options do not load external entities, which is the safe choice.
Format the output with the Ruby Formatter. For JSON data, use JSON to Ruby. Check the XML with the XML Formatter. Generation runs in your browser.