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 →