Open-source project
ohler55/ox avatar
ohler55/ox

Ox: a self-contained Ruby XML parser, marshaller and SAX engine

Ruby Optimized XML Parser

912 stars81 forksRubyMIT

At a glance

What is it?
Ox is an MIT-licensed Ruby gem that parses and writes XML, serialises Ruby objects as readable XML instead of Marshal's binary format, and offers a tunable SAX mode. It is written as a C extension that avoids libxml2, which is the whole point of the design and also its main constraint.
Who is it for?
Adopt Ox when your Ruby service parses large XML documents, marshals objects across process boundaries and you want one dependency that does not drag in libxml2. Do not adopt it for HTML scraping, for JSON, or for a Ruby version outside the 2.3 to 3.0 range its documentation lists.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 30 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The two problems Ox was written to solve

Ruby developers hit XML from two directions and dislike both. The first is parsing speed. Nokogiri is the usual answer, and it is fast, but it wraps libxml2, so the behaviour of your application depends on the libxml2 version that happens to be installed on the machine that runs it. The second problem is Marshal, Ruby's built-in object serialiser. Marshal writes a binary format, and that format changes between Ruby releases, which means a dump written by one version cannot always be read by another. It also means a message stream or a cached file is unreadable without the exact same Ruby to inspect it.

Ox addresses both. The gem is written as a C extension that uses nothing beyond standard C libraries, so there is no libxml version matrix to reason about. For serialisation, it offers a mode that writes objects as human readable XML, where each element name is a type indicator and attributes carry the class and the attribute names. The project also provides a link to a comparison of Ox, Nokogiri and LibXML, and to a page describing Ox as a faster replacement for Marshal.

The audience is narrow and specific: Ruby services that already parse XML heavily, or that need an object format that survives a Ruby upgrade and can be inspected with a text editor. It is a library, not a framework, and it does nothing for JSON.

One gem with three separate engines behind it

Ox is not one parser with a few flags. The documentation describes three distinct ways of handling an XML document, and you pick the one that fits the job.

The first is a generic XML parser and writer, which builds a document tree out of Ox::Document, Ox::Element, Ox::CData, Ox::Comment and Ox::Raw objects, then serialises it with Ox.dump. The second is a fast object marshaller, which converts a Ruby object graph to XML and back with Ox.dump and Ox.parse_obj. The third is a SAX style stream parser, driven by callbacks on a subclass of Ox::Sax and started with Ox.sax_parse.

The design choice that distinguishes Ox from a libxml2 binding is visible in the directory layout. The repository has an ext/ directory holding the C sources, a lib/ directory with the Ruby side, an ox.gemspec, a Rakefile and a build_test.sh script, plus a .clang-format file for the C code. The native part is compiled and loaded as a normal Ruby extension.

Performance claims come from the project itself and are not independently verified here. It states the parser is two or more times faster than Nokogiri, the generic XML writer as much as twenty times faster, the object serialiser up to six times faster than Marshal.dump and up to three times faster than Marshal.load, and the SAX parser forty times faster than Nokogiri with minimal Ruby callbacks. The same passage notes that different files produce slightly different timings, so treat the ratios as a reason to try it, not as a benchmark.

Installing ox and round-tripping your first object

Installation is a single gem command, as the README shows:

bash
    gem install ox

Adding it to a project is the usual Gemfile entry, and the gem is published on RubyGems as ox. Note the consequence of a C extension: the install compiles native code, so the target machine needs a working toolchain and a compiler compatible with the Ruby it will run against. That is a different deployment story from a pure Ruby gem, and it matters on locked-down CI images and minimal containers.

The first thing worth running is the object round trip, because that is the part of Ox with no equivalent in the standard library. The documentation gives a complete example:

ruby
require 'ox'

class Sample
  attr_accessor :a, :b, :c

  def initialize(a, b, c)
    @a = a
    @b = b
    @c = c
  end
end

# Create Object
obj = Sample.new(1, "bee", ['x', :y, 7.0])
# Now dump the Object to an XML String.
xml = Ox.dump(obj)
# Convert the object back into a Sample Object.
obj2 = Ox.parse_obj(xml)

The object comes back as a Sample instance with the same three attributes, including a mixed array holding a string, a symbol and a float. Because the output is XML, you can print it, diff it in a test, or hand it to something that reads XML.

Building a document tree, and the sharp edge of Ox::Raw

The generic mode gives you the same shape of API that any XML writer offers. You create a document, add an XML declaration as an Ox::Instruct node, nest Ox::Element objects, and dump the result. The documentation shows the full construction, including CDATA, a comment and a raw fragment:

ruby
require 'ox'

doc = Ox::Document.new

instruct = Ox::Instruct.new(:xml)
instruct[:version] = '1.0'
instruct[:encoding] = 'UTF-8'
instruct[:standalone] = 'yes'
doc << instruct

top = Ox::Element.new('top')
top[:name] = 'sample'
doc << top

mid = Ox::Element.new('middle')
mid[:name] = 'second'
top << mid

bot = Ox::Element.new('bottom')
bot[:name] = 'third'
bot << 'text at bottom'
mid << bot

other_elements = Ox::Element.new('otherElements')
other_elements << Ox::CData.new('<sender>John Smith</sender>')
other_elements << Ox::Comment.new('Director\'s commentary')
# other_elements << Ox::DocType.new('content')
other_elements << Ox::Raw.new('<warning>Be carefull with this! Direct inject into XML!</warning>')
top << other_elements

Look at the Ox::Raw line. That string is written into the document as is, with no escaping, and the example in the documentation carries a warning about direct injection into XML. If any part of that string comes from a user, a comment or an attribute, you have introduced an XML injection bug in your own application. The same care applies to Ox::Comment, whose text is not escaped either.

One more small signal about the API surface: the DocType line is commented out in the documentation, which suggests doctype handling is present but not something the project encourages you to lean on.

Tuning the parser with default_options for loose documents

The parser's behaviour is set through Ox.default_options, and the option that matters most is mode. The documentation describes three settings: strict, tolerant, and a mode that automatically defines missing classes, which is what you want when the XML contains element names that do not map to your Ruby classes.

HTML is the case the documentation handles explicitly, because HTML is often loose about conformance. The recommended option set is:

ruby
Ox.default_options = {
    mode:   :generic,
    effort: :tolerant,
    smart:  true
}

Three switches, and each one matters. mode selects the generic tree building parser. effort: :tolerant makes the parser accept the malformed markup that real HTML contains. smart: true turns on the smart handling of things such as unquoted attributes and empty elements.

Setting these globally is a blunt instrument, because Ox.default_options is process wide. In a Rails application that also parses XML elsewhere, a global tolerant setting leaks into code that expected strict behaviour. The gem also exposes per-call options, and the hash loading example passes mode: :hash directly to Ox.load, so the finer grained path exists when you need it.

Hash mode and the SAX path for large documents

Two shortcuts handle the common cases quickly. The first is loading XML straight into nested Ruby hashes, which avoids building an object tree at all:

ruby
require 'ox'

xml = %{
<top name="sample">
  <middle name="second">
    <bottom name="third">Rock bottom</bottom>
  </middle>
</top>
}

puts Ox.load(xml, mode: :hash)
puts Ox.load(xml, mode: :hash_no_attrs)

The two modes differ in whether attributes are kept. mode: :hash produces a structure with attribute names included, and mode: :hash_no_attrs drops them, which is what you want when the attributes are noise. Because the result is plain hashes and arrays, the output can go straight into a JSON serialiser.

The second path is the SAX parser, which never builds a tree in memory. You subclass Ox::Sax, implement the callbacks you care about, and hand the object to Ox.sax_parse:

ruby
require 'stringio'
require 'ox'

class Sample < ::Ox::Sax
  def start_element(name); puts "start: #{name}";        end
  def end_element(name);   puts "end: #{name}";          end
  def attr(name, value);   puts "  #{name} => #{value}"; end
  def text(value);         puts "text #{value}";         end
end

io = StringIO.new(%{
<top name="sample">
  <middle name="second">
    <bottom name="third"/>
  </middle>
</top>
})

handler = Sample.new()
Ox.sax_parse(handler, io)

The point of the SAX API is that you implement only the callbacks you need, so the Ruby side does no work for events you ignore. The project points at a perf_sax.rb file for an example of that tuning, and a separate Yielder example shows results being handed out during the parse rather than after it, which is the shape you want for a streaming importer.

Licence, release history, and the cases where Ox is the wrong tool

Ox is released under the MIT licence, so a copy of the copyright notice and licence text has to travel with any distribution, and the code can be vendored into a closed product. There is no copyleft obligation. For security patches the README points to a Tidelift subscription arrangement, and SECURITY.md sits in the repository root, so a security report has a defined destination.

The project is not archived and the last push was on 2026-09-23. The published gems are v2.14.27 on 2026-06-18, v2.14.28 on 2026-06-28 and v2.14.29 on 2026-08-08, and CHANGELOG.md holds the release notes. The repository also carries a CI badge, a .rubocop.yml and a .rubocop_todo.yml for the Ruby style rules, which tells you the gem is built and tested rather than hand assembled.

The documentation states compatibility with Ruby 2.3, 2.4, 2.5, 2.6, 2.7 and 3.0. No newer Ruby is listed, so before adopting the gem check the CI badge for the Ruby version your application runs on.

As for alternatives, the real comparison is architectural. Nokogiri wraps libxml2, which buys a mature, complete XML implementation and a familiar API at the cost of a native dependency whose version you do not control. Ox is self-contained C, so it is predictable across machines, and the project argues it is faster, but it is a smaller surface with fewer features. For JSON, the same author points at the oj gem, which is the closer fit. Ox is the wrong tool when you need full XML conformance features, when you need to parse HTML, or when a pure Ruby install matters more than throughput.

Editorial conclusion

Adopt Ox when your Ruby service parses large XML documents, marshals objects across process boundaries and you want one dependency that does not drag in libxml2. Do not adopt it for HTML scraping, for JSON, or for a Ruby version outside the 2.3 to 3.0 range its documentation lists. Before you commit, check the CI badge against your own Ruby version and read Ox::Raw usage in your codebase, because that helper injects XML verbatim and is the easiest way to introduce a parsing bug.

Frequently asked questions

How do I install the Ox Ruby gem?

The README gives a single command, gem install ox, and the gem is published on RubyGems as ox. Because it is a C extension, the install compiles native code, so the target machine needs a working compiler toolchain.

How is Ox different from Nokogiri for Ruby XML parsing?

Nokogiri wraps libxml2, so behaviour varies with the installed libxml2 version, while Ox is self-contained and uses only standard C libraries. The project states Ox is two or more times faster as a parser and as much as twenty times faster as a generic XML writer, with different files giving different timings.

Can Ox replace Ruby's Marshal for object serialization?

Yes, that is one of its three modes. Ox.dump writes an object graph as human readable XML where element names are type indicators and attributes carry class and attribute names, and Ox.parse_obj reads it back, avoiding the version specific binary format of Marshal.

How do I parse HTML with Ox?

Set the default options to a tolerant mode, as in the documentation example with mode: :generic, effort: :tolerant and smart: true. Those settings accept the loose markup that HTML files usually contain.

Which Ruby versions does Ox support?

The documentation lists Ruby 2.3, 2.4, 2.5, 2.6, 2.7 and 3.0. No newer version is mentioned, so check the CI badge in the repository against the Ruby your application uses.

Official sources

  1. License: MIT
  2. ohler55/ox on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/ohler55-ox.svg)](https://hysenlabs.com/projects/ohler55-ox)