Skip to content

XML ↔ JSON Converter — Ruby source

Convert XML to JSON and back, preserving attributes. Validates input and reports errors clearly, runs entirely in your browser, with a shareable link to your exact input.

This is the Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.

# xml-to-json — bidirectional XML <-> JSON converter. Ruby port of src/lib/xml-to-json.ts — same logic as this dir's rust.rs (Go-twin contract); CDATA/mixed-content text and entity decoding simplified for the 80-line budget (see rust.rs / python.py).
require 'json'
require 'rexml/document'

# REXML element -> JSON value: attributes become "@_<name>" keys, a text-only
# element becomes its bare string, an element with attributes/children becomes
# an object whose direct text lands under "#text", and repeated child tags
# collapse into an array.
def element_value(el)
  attrs = el.attributes.to_h { |k, v| ["@_#{k}", v] }
  kids = el.elements.to_a
  return el.text.to_s.strip if kids.empty? && attrs.empty?

  obj = attrs
  kids.each do |kid|
    v = element_value(kid)
    obj[kid.name] = if obj.key?(kid.name)
                      (obj[kid.name].is_a?(Array) ? obj[kid.name] : [obj[kid.name]]) << v
                    else
                      v
                    end
  end
  text = el.text.to_s.strip
  obj['#text'] = text unless text.empty?
  obj
end

# Parse well-formed XML to pretty JSON, or nil if REXML rejects the document
# (mismatched tags, no root, trailing junk all raise ParseException).
def xml_to_json(xml)
  doc = REXML::Document.new(xml)
  return nil if doc.root.nil?
  JSON.pretty_generate(doc.root.name => element_value(doc.root))
rescue REXML::ParseException
  nil
end

# Build indented XML from a JSON string with an object root, or nil if the
# JSON does not parse or the root is not an object.
def json_to_xml(json)
  obj = JSON.parse(json)
  return nil unless obj.is_a?(Hash)

  build = lambda do |tag, value, depth|
    ind = '  ' * depth
    if value.is_a?(Hash)
      attrs, kids, text = +'', [], +''
      value.each do |k, v|
        case k
        when /\A@_/  then attrs << " #{k[2..]}=\"#{v}\""
        when '#text' then text << v.to_s
        else kids << [k, v]
        end
      end
      next "#{ind}<#{tag}#{attrs}></#{tag}>\n" if kids.empty? && text.empty?
      next "#{ind}<#{tag}#{attrs}>#{text}</#{tag}>\n" if kids.empty?
      inner = kids.flat_map { |k, v| v.is_a?(Array) ? v.map { |i| [k, i] } : [[k, v]] }
      "#{ind}<#{tag}#{attrs}>\n#{inner.map { |k, v| build.call(k, v, depth + 1) }.join}#{ind}</#{tag}>\n"
    else
      "#{ind}<#{tag}>#{value}</#{tag}>\n"
    end
  end
  obj.map { |k, v| build.call(k, v, 0) }.join
rescue JSON::ParserError, TypeError
  nil
end

if $PROGRAM_NAME == __FILE__
  xml = '<user id="7"><name>Alice</name><pet>cat</pet><pet>dog</pet></user>'
  puts xml_to_json(xml)
  puts json_to_xml('{"user":{"@_id":"7","name":"Alice"}}')
  puts(xml_to_json('<a><b></a>') || 'nil') # mismatched close -> nil
end

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →