Jsonify: XML to JSON¶
pygixml.jsonify serializes XML directly to JSON. “Directly” is the
operative word: the in-memory entry points (dumps()
and friends) traverse the pugixml DOM in C++ and write straight into a
JSON string buffer: no intermediate Python dict/list is ever
built, unlike going through pygixml.dictify.parse() followed by
json.dumps(). The streaming entry points
(stream_dump(),
stream_jsonl()) go a step further and skip
even the DOM, converting a giant XML file to a giant JSON file in
roughly constant memory.
The output shape matches pygixml.dictify.parse() exactly (same
@-prefixed attributes, #text for mixed content, repeated
siblings collapsed into arrays): jsonify.dumps(xml) is equivalent
to, but faster than, json.dumps(dictify.parse(xml)).
from pygixml import jsonify
xml = """
<database name="users_db">
<user id="101">
<name>Mohammad</name>
<balance>450.75</balance>
</user>
<tag>active</tag>
<tag>verified</tag>
</database>
"""
jsonify.dumps(xml)
# '{"database": {"@name": "users_db", "user": {"@id": "101", ...}, "tag": ["active", "verified"]}}'
jsonify.dumps(xml, pretty=True, indent=" ") # multi-line, 2-space indent
In-memory entry points¶
- pygixml.jsonify.dumps(source, attr_prefix='@', cdata_key='#text', force_list=None, pretty=False, indent='\\t', encoding='utf-8')
Smart dispatcher: serializes XML to JSON regardless of what form the XML is already in:
strstarting with<→ parsed and serialized (same asdumps_str())ObjectifiedElement→ serialized directly from the live DOM subtree (same asdumps_obj())XMLNode→ serialized directly (same asdumps_node())
- Parameters:
source -- XML string, an already-parsed
ObjectifiedElement, or anXMLNode.attr_prefix (str) -- Prefix prepended to attribute keys. Default
"@".cdata_key (str) -- Key used for text content in mixed nodes. Default
"#text".force_list (set or True or None) -- Tag names that should always be wrapped in a JSON array, even when only one sibling exists. Pass
Trueto force every tag.pretty (bool) -- Indent the output. Default
False(compact).indent (str) -- Indentation string used when
pretty=True. Default a tab.
- Returns:
JSON string.
- Return type:
str
- Raises:
PygiXMLError -- If the XML is malformed.
TypeError -- If
source’s type isn’t recognized.ValueError -- If
sourceis astrthat doesn’t look like XML (file paths are rejected here on purpose: usedumps_file()explicitly for files).
Note
File input is intentionally excluded from the dispatcher: call
dumps_file()directly for a path, so it’s always unambiguous whether astrargument is XML content or a file path.
- pygixml.jsonify.dumps_str(xml, attr_prefix='@', cdata_key='#text', force_list=None, pretty=False, indent='\\t', encoding='utf-8')
Parse an XML string and serialize it directly to JSON.
- Parameters:
xml (str) -- XML source text.
- Returns:
JSON string.
- Return type:
str
- Raises:
PygiXMLError -- If the XML is malformed.
- pygixml.jsonify.dumps_file(path, attr_prefix='@', cdata_key='#text', force_list=None, pretty=False, indent='\\t', encoding='utf-8')
Parse an XML file and serialize it directly to JSON, returning the result as a
str. For files too big to hold as a JSON string in memory, seestream_dump()instead.- Parameters:
path (str) -- Filesystem path to the XML file.
- Returns:
JSON string.
- Return type:
str
- Raises:
PygiXMLError -- If the file cannot be read or the XML is malformed.
- pygixml.jsonify.dumps_obj(elem, attr_prefix='@', cdata_key='#text', force_list=None, pretty=False, indent='\\t', encoding='utf-8')
Serialize an already-parsed
ObjectifiedElementsubtree directly to JSON, without re-parsing or re-traversing as a dict first.- Parameters:
elem (pygixml.ObjectifiedElement) -- Element to serialize.
- Returns:
JSON string.
- Return type:
str
- Raises:
TypeError -- If
elemis not anObjectifiedElement.
from pygixml import objectify, jsonify root = objectify.from_string(xml) jsonify.dumps_obj(root.user) # just the <user> subtree
- pygixml.jsonify.dumps_node(node, attr_prefix='@', cdata_key='#text', force_list=None, pretty=False, indent='\\t', encoding='utf-8')
Serialize a low-level
XMLNodedirectly to JSON.- Parameters:
node (pygixml.XMLNode) -- Node to serialize.
- Returns:
JSON string.
- Return type:
str
- Raises:
TypeError -- If
nodeis not anXMLNode.
Streaming entry points: constant memory, files in and out¶
The functions above all hold the result (and, except for
dumps_obj/dumps_node, the parsed DOM too) in memory: fine for
documents that fit comfortably in RAM. For documents that don’t,
jsonify has two streaming converters that go file-to-file, entirely
in C++, with no pugixml DOM, no Python dict/list/str for
individual elements, and no json module anywhere in the call chain.
- pygixml.jsonify.stream_dump(xml_path, json_path, attr_prefix='@', cdata_key='#text', force_list=None, indent=0, stack_size=4096, io_buf_size=65536)
Convert a (potentially gigantic) XML file into a single, standard, valid JSON file: in roughly constant memory. Produces exactly what
dumps_file()would produce (one JSON value mirroring the whole document, loadable with a plainjson.load), just without ever holding the document, or the output, fully in memory.- Parameters:
xml_path (str) -- Path to the input XML file.
json_path (str) -- Path to the output JSON file. Overwritten if it exists.
attr_prefix (str) -- Prefix for XML attribute names in JSON keys. Default
"@".cdata_key (str) -- JSON key used for text content mixed with attributes or child elements. Default
"#text".force_list (set or True or None) -- Tag names always serialized as a JSON array.
Trueforces every tag. DefaultNone(a tag becomes an array only once a second sibling with that name actually appears).indent (int) -- Spaces per nesting level, same convention as
json.dump(..., indent=N).0(default) is compact; any positive value pretty-prints.stack_size (int) -- Size in bytes of yxml’s internal name stack.
io_buf_size (int) -- Bytes read per XML I/O operation. Default 64 KB.
- Returns:
Number of XML elements processed (informational).
- Return type:
int
- Raises:
PygiXMLError -- On malformed XML, or if the input/output file cannot be opened.
How it stays constant-memory while still producing valid JSON syntax. A JSON array needs to know, before its closing
], whether more items follow: but the parser only finds that out when (and if) a second same-tag sibling actually shows up. Rather than buffer whole subtrees to be safe, the engine writes optimistically and patches the output file in place once it learns more:The first time a child tag is seen under some parent, nothing extra is written at all: just its value, as a plain (non-array) field. No placeholder, no reserved byte; a tag that never repeats costs nothing beyond its own content.
A second sibling with the same tag arrives:
If it’s adjacent to the first (the common case for record-oriented XML: same-tag siblings next to each other in the source), the engine inserts the opening
[and the new value right where the first value ended: an O(1) operation, and the only one this case ever needs.If a different child tag was interleaved in between, the engine splices: it shifts the interleaved bytes forward (in fixed-size chunks) to open room for
[and the new value. Cost is proportional to how much was interleaved that one time, not to the file size.
Time complexity in practice. For the shape almost all record-oriented giant XML actually has: one repeated tag per nesting level, as in
<orders><order>...</order>...</orders>: every sibling after the first lands via the O(1) adjacent case above, so the whole conversion is O(n). It degrades towards O(n²) only in an adversarial shape: two or more different tags repeating and interleaving at the same level for many iterations (e.g.<a>1</a><b>1</b><a>2</a><b>2</b>...with no wrapping element around each pair): there, every catch-up splice for one tag has to shift past the other tag’s entire, still-growing field, and that cost compounds across iterations. If that’s genuinely your data’s shape,stream_jsonl()below sidesteps the problem entirely by not trying to preserve a single JSON document at all. Memory stays O(1): a small fixed scratch buffer plus one bookkeeping entry per currently-open distinct tag: regardless of which case applies; only the time bound changes.from pygixml import jsonify import json jsonify.stream_dump("huge.xml", "huge.json") # compact jsonify.stream_dump("huge.xml", "huge.json", indent=2) # pretty with open("huge.json") as f: data = json.load(f) # a single, ordinary, valid JSON document
- pygixml.jsonify.stream_jsonl(xml_path, jsonl_path, tag, attr_prefix='@', cdata_key='#text', force_list=None, stack_size=4096, io_buf_size=65536)
The file-to-file counterpart of
iterjsonl()(and the per-tag, filtering complement tostream_dump(), which always converts the entire document): streams an XML file straight to a.jsonlfile, one matched element per line, entirely in C++. Unlikeiterjsonl(), noStreamElementand no Pythonstr/dict/listis ever created for the matched elements themselves: each element’s JSON object is assembled in a small in-memory buffer (bounded by that one element’s own subtree, the same constant-memory model asiterfind()) and written straight to the file.- Parameters:
xml_path (str) -- Path to the source XML file.
jsonl_path (str) -- Path to the
.jsonlfile to write. Overwritten if it exists.tag (str) -- Tag name of the elements to convert and write, one per line.
attr_prefix -- Same meaning as
stream_dump().cdata_key -- Same meaning as
stream_dump().force_list -- Same meaning as
stream_dump().stack_size (int) -- Size in bytes of yxml’s internal name stack.
io_buf_size (int) -- Bytes read per XML I/O operation. Default 64 KB.
- Returns:
Number of matched elements written.
- Return type:
int
- Raises:
PygiXMLError -- On malformed XML, or if the input/output file cannot be opened.
Note
If
tagappears nested inside an already-matched element, that inner occurrence is folded into the outer match as an ordinary nested field (under its own tag-name key) rather than written as a second, separate line: only the outermost occurrence of a match starts a new JSONL record. This only matters for genuinely self-nested tags; a flat list of repeated sibling records (the common case) is unaffected.from pygixml import jsonify n = jsonify.stream_jsonl("huge.xml", "huge.jsonl", "record") print(f"wrote {n} records") import json with open("huge.jsonl") as f: for line in f: record = json.loads(line) # each line is independent, valid JSON
Choosing the right entry point¶
You have / want |
In memory |
Streamed (constant memory) |
|---|---|---|
Whole document → one JSON value |
||
One record per line, output as a |
Loop + write yourself (see
|
|
One record per line, kept as Python |
(inherently produces a Python object per line: see Streaming: Constant-Memory Parsing for Big XML for the all-C++ alternative when that’s not needed) |
|
Already-parsed |
(n/a: already in memory) |
See Streaming: Constant-Memory Parsing for Big XML for iterjsonl and the rest of the underlying
constant-memory parsing layer that stream_dump and
stream_jsonl are built on top of.