# Data conversion
```{eval-rst}
.. meta::
:description: Convert bound XML to and from five JSON formats, with explicit scalar, namespace, fidelity, and document validation rules.
```
`Document.to_data()` exports the bound tree as JSON-safe Python values.
`Document.to_json()` encodes the same values as JSON text. Choose a format:
- `format="pyxsd"` (the default) keeps the native {doc}`data-model`
structure: it absorbs the root name and groups children by name.
- `format="pyxsd-json"` is pyxsd's schema-driven format. It retains the root
name and namespaces, and uses the XSD rather than observed child counts to
choose scalar, object, or array layouts. Child order and mixed text positions
are preserved where grouped formats would lose them.
- `format="parker"` ([Parker][parker]) absorbs the root, omits attributes,
and groups children by local name.
- `format="badgerfish"` ([BadgerFish][badgerfish]) retains the root,
attributes, and namespace bindings in a grouped structure.
- `format="jsonml"` ([JsonML][jsonml]) uses nested arrays to retain tags,
attributes, and text/element order.
These exports need no optional dependencies. The {doc}`integrations`
provide a separate Pydantic model projection and Arrow/Parquet records;
their aliases and presence rules are not these document formats.
`Schema.from_data()` and `Schema.from_json()` import all five formats into
an ordinary `Document`. Import defaults to `format="pyxsd-json"`, unlike
export's native `"pyxsd"` default; select the format explicitly when changing
between them.
`Schema.json_schema()` constructs a Draft 2020-12 description for any of
these formats directly from the compiled XSD, without a sample document.
## Export a document
This complete example uses in-memory schema and XML inputs:
```python
import json
from io import StringIO
import pyxsd
schema = pyxsd.compile(
StringIO("""
""")
)
document = schema.parse(StringIO("2026-01-012026-02-01"))
document.require_valid()
assert document.to_data() == {"date": ["2026-01-01", "2026-02-01"]}
data = document.to_data(format="pyxsd-json")
assert data == {"dates": ["2026-01-01", "2026-02-01"]}
text = document.to_json(format="pyxsd-json", indent=2)
assert json.loads(text) == data
assert document.to_data(format="parker", fidelity="strict") == {
"date": ["2026-01-01", "2026-02-01"]
}
assert document.to_data(format="badgerfish", fidelity="strict") == {
"dates": {"date": ["2026-01-01", "2026-02-01"]}
}
assert document.to_data(format="jsonml", fidelity="strict") == [
"dates",
["date", "2026-01-01"],
["date", "2026-02-01"],
]
```
Both methods accept keyword-only `format`, `fidelity="best-effort"`,
`typed=True`, and `always_list=False`. `to_json()` also accepts `indent=None`.
`typed` and `always_list` are native-only options: `typed=False` or
`always_list=True` with any other format raises `ValueError`.
Export reads the current bound tree; it neither revalidates it nor requires
a valid report. Call `require_valid()` when validity is a prerequisite, or
follow {doc}`quickstart`'s `revalidate()` workflow after edits. A document
without a bound root raises `PyXSDError`.
## Import data or JSON
Both methods return a `Document` and have these signatures (`JsonValue` is
a JSON-compatible Python value):
```text
Schema.from_data(
self, data: JsonValue, *, format: FormatName = "pyxsd-json",
element: str | None = None, namespaces: Mapping[str, str] | None = None,
fidelity: Fidelity = "strict",
) -> Document
Schema.from_json(
self, source: str | bytes | bytearray, *, format: FormatName = "pyxsd-json",
element: str | None = None, namespaces: Mapping[str, str] | None = None,
fidelity: Fidelity = "strict",
) -> Document
```
`from_data()` accepts dictionaries with string keys, lists, strings,
booleans, integers, finite floats, and `None`, subject to the selected
format's root, layout, and scalar rules below. `from_json()` accepts JSON
content as `str`, UTF-8 `bytes`, or `bytearray`. It does not open a filename,
URL, `Path`, or file-like object; unsupported source types raise `TypeError`.
Read files explicitly with
`Path("data.json").read_text(encoding="utf-8")` or `.read_bytes()`, then pass
the result to `from_json()`.
JSON parsing rejects duplicate object keys at any depth, malformed JSON,
non-UTF-8 bytes, and bare `NaN`, `Infinity`, or `-Infinity` tokens. XSD
non-finite values instead use the JSON strings `"NaN"`, `"INF"`, and
`"-INF"`.
Direct Python input rejects non-finite floats too, including nested values.
Import defaults to `fidelity="strict"`, unlike export's `"best-effort"`.
Only the [import fallbacks](#import-fidelity) below are permitted in best
effort; it does not coerce wrong scalar types, repair containers, discard
content, or suppress validation. Unknown format or fidelity names raise
`ValueError`. Neither import method accepts `typed` or `always_list`;
supplying them raises `TypeError`.
### Round trip through a document
Continuing the dates example above:
```python
restored = schema.from_data(data)
restored.require_valid()
assert restored.root is not None
assert [str(date) for date in restored.root.date] == ["2026-01-01", "2026-02-01"]
assert restored.to_data(format="pyxsd-json") == data
for source in (text, text.encode("utf-8"), bytearray(text.encode("utf-8"))):
assert schema.from_json(source).to_data(format="pyxsd-json") == data
xml = restored.to_string()
reparsed = schema.parse(StringIO(xml))
reparsed.require_valid()
assert reparsed.to_data(format="pyxsd-json") == data
checked = restored.revalidate()
checked.require_valid()
assert checked.to_data(format="pyxsd-json") == data
for format in ("pyxsd", "parker", "badgerfish", "jsonml"):
exported = document.to_data(format=format, fidelity="strict")
imported = schema.from_data(exported, format=format, element="dates")
imported.require_valid()
assert imported.to_data(format=format, fidelity="strict") == exported
assert schema.from_json(json.dumps(exported), format=format).is_valid
```
Import builds an ElementTree, records its namespace frames, and calls the
normal `Schema.parse()` binder. The result exposes generated classes,
datatype values, defaults, validation reports, transforms, and XML output
just like an XML-parsed document. Empty lexical content can activate an
element default; omitted attributes receive the binder's normal effective
defaults. See {doc}`data-model` for attribute presence versus defaults.
Round trips preserve represented values and order, not source bytes or
every input spelling. Defaults may replace empty input, adjacent mixed-text
strings merge, and export normalizes layout and scalar spellings.
### Root selection
- `pyxsd-json` requires a single-entry root object whose key is an EQName
(`Q{uri}local`) or unnamespaced local name, not a lexical prefix or Clark
name. The wrapper is required even with only one global declaration.
- BadgerFish also requires a single-entry root object, but its key is a
lexical QName resolved against its namespace declarations.
- JsonML requires a nonempty array starting with a lexical tag string.
- Native and Parker absorb the root. Supply `element` when there is not
exactly one concrete global root; otherwise selection raises
`root-ambiguous`. A native mapping is content, not a root wrapper.
An empty or multi-entry root object in the root-carrying formats raises
`root-ambiguous`; a wrong outer container raises `container-invalid`.
The optional `element` selects an absorbed root or confirms a carried one.
It accepts a local name, Clark name (`{uri}local`), or EQName. A local
selection must resolve unambiguously among global declarations
(`name-ambiguous` otherwise).
For a carried root, selecting a substitution head also accepts its valid
substitution members; a contradictory selection raises `root-mismatch`.
`element` never renames a carried root. An unknown root without a
contradictory selection reaches the binder's `unknown-root` report and can
return a document whose `root` is `None`.
### Schema-driven layouts and scalar input
Import reads the `pyxsd-json` layouts described below:
- Simple values use the fixed scalar policy, including decimal strings
and XSD-list arrays. A boolean is not an integer, and `1.0` is not an
integer input. There is no general lexical-string coercion: `"42"` does
not stand in for an integer JSON number.
- List layouts create one child per array item, retaining each child's
attributes. Non-nil list-plus objects require their child-name array,
including `[]` when empty.
- Record properties become children in schema order, regardless of JSON
object order. Sequence arrays retain supplied child order and
interleaving. Attribute maps must precede content; each child map has
exactly one entry.
- Mixed arrays retain leading text, child elements, and tails. Adjacent
strings merge into one text region. In element-only sequence input,
whitespace-only strings are discarded; non-whitespace strings reach
normal content validation rather than disappearing.
- `None`/`null` requests nil. Object and ordered layouts also accept their
`#content: null` markers as shown below. Nil cannot carry other content;
a non-nillable target is a validation problem, not a conversion failure.
An explicit `@xsi:type` EQName selects the type before layout decoding,
including promotion to a plus layout. Substitution members use their own
declarations. Conditional type assignment happens in the normal binder;
the subsequent export uses the selected effective type's layout.
### Conventional input
The [native](#native-pyxsd-structure), [Parker](#parker-structure),
[BadgerFish](#badgerfish-structure), and [JsonML](#jsonml-structure) sections
below define their structures. Scalar input follows the
[scalar table](#scalar-values): for example, decimals require strings,
booleans are not integers, and `1.0` is not integer input. Native lexical
strings are the compatibility exception. Import adds these rules:
- **Native:** accepts both typed values and `typed=False` lexical strings,
including integer/boolean strings and space-separated XSD lists. Boolean
content also accepts legacy integer `0`/`1`, which native export retains;
other integers are not boolean inputs. A simple
root may be a bare scalar or `{"$": value}`. Grouped child arrays accept
native `always_list=True` output, including one-item arrays. Local child
and attribute names resolve through a unique schema declaration; ambiguous
locals raise `name-ambiguous` in both modes. Clark names identify the
namespace directly; unresolved locals remain unqualified for validation.
Ordinary prefixed keys are not accepted. `@xsi:type` and `@xsi:nil` (or
their Clark-key equivalents) are exceptions: the former takes a lexical
QName; the latter accepts a boolean or lexical string. A true nil marker
permits other attributes but conflicts with any `$` or child property.
- **Parker:** `{}` supplies empty element-only content, `""` an empty simple
string, `[]` an empty XSD list, and `null` always requests nil. Children use
local names only: prefixed/Clark keys and `@` attributes are `name-invalid`.
`$` is accepted only for mixed content. A local child must resolve uniquely
through the schema; unresolved or ambiguous names require the warned
best-effort fallback below. No attributes are constructed; defaults still
apply, and missing required attributes remain validation errors.
- **BadgerFish:** accepts scalar leaves or `$`-wrapped text, `@` attributes,
and grouped children. Repeatable child properties accept occurrence arrays
(including `[]`) or a single non-array occurrence; export normalizes a
single occurrence to an array. `@xsi:nil` requires a JSON boolean and its namespace
binding. When true, it retains other attributes but conflicts with any `$`
or child property, even empty content. Bare `null` is also accepted as nil;
export uses the explicit attribute marker. `@xsi:type` takes a lexical QName
and selects the actual type before decoding content.
- **JsonML:** reads attributes only from the optional object at index 1.
Remaining nested arrays, direct text, and tails retain supplied order,
including mixed whitespace; adjacent strings merge. Whitespace-only
element-only text is discarded, while other text reaches validation.
Simple content accepts one typed scalar item, or one array-valued item for
an XSD list type, rather than interpreting it as a child tag. A sole `null`
content item requests nil; combining nil with other content raises
`marker-conflict`. The `xsi:type` attribute takes a lexical QName.
For a repeatable **list-valued** child, nested arrays separate occurrences
from list items: `{"refs": [["a", "b"], ["c"]]}` represents two elements.
BadgerFish always uses the outer occurrence array, including `[]` for zero
occurrences. Native and Parker accept nonempty nested occurrence arrays;
their flat arrays (including `[]`) remain ambiguous between one list value
and repeated elements and raise `container-invalid` in both modes. Omit the
property for zero occurrences. Native `always_list=True` supplies the outer
array even for one occurrence. For a non-repeatable list-valued child, the
array is one scalar value; native also accepts its one-item outer wrapper.
Native element `null` needs special care: a non-nillable target becomes
empty content; a nillable target becomes nil when empty content is not
possible. If both empty and nil are possible, strict raises
`empty-nil-ambiguous`; best effort warns and chooses nil. Empty-content
possibility depends on the schema, including defaults, required attributes,
and optional content. To request empty content explicitly, use `{}` or
`{"$": ""}` (`{"$": null}` also means empty text, not nil). Use
`{"@xsi:nil": true}` for an explicit nil request.
### Import fidelity
Native, Parker, and BadgerFish reconstruct grouped children in schema order
for supported models, including fixed sequences with distinct child names
and `xs:all` records. Array items retain occurrence order within their group.
Strict raises for each loss below; best effort emits `PyXSDWarning` and
applies only the listed fallback:
| Format | Code | Import fallback |
| --- | --- | --- |
| Native, Parker, BadgerFish | `order-ambiguous` | Use mapping iteration order and expand arrays at their property position; do not invent interleaving. |
| Native, Parker, BadgerFish | `mixed-content` | Put concatenated `$` text before the grouped children; lost text positions cannot be restored. |
| Parker | `name-ambiguous` | Leave an unresolved or ambiguous child name unqualified for normal validation. |
| Native, Parker, BadgerFish, JsonML | `namespace-unresolved` | Leave an unresolved lexical QName value to the binder. BadgerFish and JsonML also do this for unresolved names and `xsi:type`. |
| Native | `empty-nil-ambiguous` | Interpret ambiguous element `null` as nil. |
Repeated models allowing several child names can require the ordering
fallback even if a particular object looks ordered. When multiple distinct
child names are present, grouped input also conservatively rejects a present
expanded child name with multiple possible content-model positions, such as
`a` in `a?, b, a?`, even if occurrence counts could force one allocation.
Strict raises `order-ambiguous`; best effort warns and uses mapping order.
Use `pyxsd-json` or JsonML when explicit interleaving is needed.
Mixed content with multiple distinct child names also loses child order;
text-only mixed content needs no positional fallback. JsonML represents order
directly and needs neither ordering nor mixed-text fallback. `pyxsd-json` has
no import fallbacks: both fidelities use the same decoding rules without loss
warnings.
### Namespaces and QName values
A `namespaces` mapping seeds the root's inherited prefix-to-URI frame for
every format. Native and Parker need it for prefixed QName values; it does
not enable prefixed child keys. Native resolves `@xsi:type` when its prefix
is bound; an unresolved type prefix instead reaches the binder's
`unknown-namespace-prefix` report, even in strict mode.
BadgerFish merges `@xmlns` declarations (`$` denotes the default namespace);
JsonML merges `xmlns` and `xmlns:prefix` from the attribute object. Nested
declarations override inherited bindings and apply to the element carrying
them, including its own tag, and its descendants. Each BadgerFish occurrence
in a repeated array has its own frame; sibling declarations do not leak.
Unprefixed attributes
never use the default namespace. Required bindings for names, QName values,
and `xsi:type` must resolve in strict mode. Import accepts supplied prefixes
and default namespaces; the deterministic explicit prefixes described
below are export choices, not input requirements.
In `pyxsd-json`, element and attribute keys use EQNames, including wildcard
names; `namespaces` does not change that contract. QName values, QName-list
items, and `@xsi:type` values can carry their
namespace directly as EQNames. Import translates these to lexical XML
QNames and records inherited/generated bindings on every element before
binding. Existing suitable prefixes are reused; generated prefixes avoid
collisions. Original prefix spelling is not preserved.
This complete example needs no caller-supplied prefix declarations:
```python
from io import StringIO
import pyxsd
qname_schema = pyxsd.compile(
StringIO("""
""")
)
qname_data = {"Q{urn:example}ref": "Q{urn:values}item"}
qname_document = qname_schema.from_data(qname_data, element="{urn:example}ref")
qname_document.require_valid()
assert qname_document.to_data(format="pyxsd-json") == qname_data
qname_copy = qname_schema.from_json(qname_document.to_json(format="pyxsd-json"))
qname_copy.require_valid()
assert qname_copy.to_data(format="pyxsd-json") == qname_data
qname_reparsed = qname_schema.parse(StringIO(qname_document.to_string()))
qname_reparsed.require_valid()
assert qname_reparsed.to_data(format="pyxsd-json") == qname_data
```
QName JSON round trips retain expanded names (namespace URI and local name).
In namespace-aware mode, `to_string()` also preserves resolved QName values
in element content, declared attributes, XSD lists, and selected union members, for both imported
and XML-parsed documents. The writer declares namespaces used only by these
values and re-spells the values with its selected prefixes, including values
originally resolved through a default namespace. Prefixes and source bytes
are not preserved; compare expanded names rather than lexical spellings.
Unresolved QName values pass through unchanged; the writer does not invent
missing namespace bindings.
Actual types selected by `xsi:type` also contribute value-only namespaces;
the XML writer declares those namespaces and re-spells the type marker.
This also applies to redundant native markers equal to the declared type.
An unresolved marker is retained, not repaired by guessing the bound class.
The same schema must not import or parse concurrently or reentrantly.
Separate `Schema` objects can compile and parse concurrently.
### Conversion errors versus validation reports
In every format, conversion raises `PyXSDError` when a trustworthy tree
cannot be built: for example, duplicate JSON keys (`json-duplicate-key`),
malformed JSON (`json-invalid`), invalid names (`name-invalid`), wrong containers
(`container-invalid`), wrong scalar types (`scalar-type-invalid`), or
conflicting nil/content markers (`marker-conflict`). Diagnostics include a
reason code, selected format, and JSON Pointer path; the empty pointer identifies the whole
input. JSON parsing errors use that empty pointer. Best-effort warnings
carry reason codes and paths too; they do not become validation issues.
Ambiguous root selection, malformed names, wrong containers/scalar types,
and duplicate or conflicting markers always raise in either fidelity mode.
Correctly shaped content still undergoes XSD validation. Invalid lexical
strings for string-represented datatypes, missing required content,
unknown children or attributes in decodable containers, unresolved actual
types, and non-nillable nil requests reach `Document.report`. Parker names
must first resolve or use its warned fallback. Import does not drop unknown
content or automatically call `require_valid()`.
Inspect the report, or call `require_valid()` to raise `ValidationError`.
Using the dates schema from the first example:
```python
from pyxsd.exceptions import PyXSDError, ValidationError
try:
schema.from_data({"dates": [5]})
except PyXSDError as error:
assert "scalar-type-invalid at '/dates/0'" in str(error)
else:
raise AssertionError("A date requires a JSON string")
invalid = schema.from_data({"dates": ["not-a-date"]})
assert invalid.root is not None
assert invalid.report.has_errors
try:
invalid.require_valid()
except ValidationError:
pass
else:
raise AssertionError("Invalid date content must fail validation")
```
## Construct a JSON Schema
`Schema.json_schema(*, format="pyxsd-json", element=None, fidelity="strict",
typed=True, always_list=False)` returns a JSON-safe dictionary. Generation
needs no optional dependency. To check data with the separate
[`jsonschema`](https://python-jsonschema.readthedocs.io/en/stable/) package,
install it with `pip install jsonschema`.
Continuing the dates example:
```python
from jsonschema import Draft202012Validator
for format in ("pyxsd", "pyxsd-json", "parker", "badgerfish", "jsonml"):
contract = schema.json_schema(format=format, element="dates")
Draft202012Validator.check_schema(contract)
exported = document.to_data(format=format, fidelity="strict")
Draft202012Validator(contract).validate(exported)
native_contract = schema.json_schema(format="pyxsd", element="dates", typed=False, always_list=True)
Draft202012Validator(native_contract).validate(
document.to_data(format="pyxsd", typed=False, always_list=True)
)
```
### Roots and export options
`element` accepts the same local, Clark, and EQName selections as import;
selecting a substitution head includes its permitted members. An ambiguous
local name raises `name-ambiguous`; malformed names raise `name-invalid`.
Without `element`, root-carrying `pyxsd-json`, BadgerFish, and JsonML describe
all concrete global roots, using `anyOf` for alternatives. No concrete roots
produces a schema that rejects every value. Native and Parker instead require
exactly one concrete global root or an explicit selection (`root-ambiguous`
otherwise). An unknown explicit selection yields a general format shape,
not evidence that the XSD declares that root.
Match native `typed` and `always_list` to the intended export. Lexical mode
describes strings, including space-separated XSD lists; `always_list=True`
requires arrays for child properties. Other formats reject non-default values
for these options with `ValueError`. There is no `namespaces` override.
The schemas describe the [structures below](#native-pyxsd-structure), not
every spelling import accepts. In particular, native primitive roots have
their `$` wrapper, BadgerFish schema-repeatable children use arrays and its
nil marker is boolean, and JsonML fixes tags with `prefixItems`. Generated
namespace-declaration properties describe root-scoped export, not every
nested namespace frame accepted on import. Native `$` text inside an object
is lexical even in typed mode; BadgerFish `$` simple content stays typed.
Parker keeps empty element-only `{}` distinct from nil `null`.
### Metadata and validation limits
Every result includes `$schema: "https://json-schema.org/draft/2020-12/schema"`,
`x-pyxsd-format`, and `x-pyxsd-validation` with `level: "structural"` and a
sorted `unvalidated` list. Only `pyxsd-json` adds `x-pyxsd-profile-version: 1`.
Reusable and recursive shapes use deterministic `$defs`/`$ref`; annotations
such as `x-pyxsd-qname`, `x-pyxsd-type`, and `x-pyxsd-layout` identify their
XSD origin or representation.
Generation encodes scalar types, expressible bounds, enumerations, lengths,
and tested pattern equivalents. It discloses remaining rules as applicable:
`xsd-particle-order`, `xsd-mixed-order`, `xsd-wildcard-namespace`,
`xsd-conditional-type`, `xsd-identity-constraints`, `xsd-assertions`,
`xsd-regex-dialect`, and `xsd-facet`. Native typed null ambiguity adds
`native-null-ambiguity`; Parker QName context loss adds `namespace-lost`.
Lexical QName schemas carry `x-pyxsd-namespace-context: true`; matching their
pattern does not resolve their prefixes. JSON Schema `format` and
`contentEncoding`, when present, are annotations unless a validator enforces
them.
The description is deliberately broader than the conversion rules in places.
Sequence/mixed item alternatives do not enforce every particle, cardinality,
or attribute-first rule. JsonML's optional attribute object shifts content
positions; tag and item schemas do not fully validate that placement.
Native and BadgerFish merge effective-type properties without expressing
every dependency between an actual type, required attributes, text, and nil.
For nillable parents, their child properties are not unconditionally required:
a nilled element has no children. Required attributes remain required.
Even an empty `unvalidated` list is not a promise of complete XSD validation
or import acceptance. Import with the matching format, inspect the report,
and call `require_valid()` for the normal XSD checks.
Both generation fidelities currently produce the same description for
supported compiled schemas, without loss warnings. `fidelity="strict"`
does not make JSON Schema an XSD validator, nor does successful generation
guarantee that strict export or import can represent a particular document.
This API is separate from the optional {doc}`integrations`:
`ModelSet` models' `model_json_schema()` describes Pydantic's aliases,
presence rules, and supported projection shapes, not these five document
formats. Neither projection changes the other's contract.
## Scalar values
With the default `typed=True`, all five formats use these JSON-safe values:
| Bound value | JSON value |
| --- | --- |
| Boolean | Native: integer `1` or `0`; other formats: `true` or `false` |
| Integer and integer-derived types | Integer number |
| Decimal | String, never a binary float; new formats use fixed-point; native retains legacy `str()` spelling |
| Finite float or double | Number |
| Non-finite float or double | `"INF"`, `"-INF"`, or `"NaN"` |
| XSD list value | Array, including zero or one item |
| QName | EQName string in `pyxsd-json`; lexical QName in the other formats |
| Other atomic values, including dates, durations, binary values, and URIs | Datatype lexical string |
Python data uses `bool` and `None` where JSON uses booleans and `null`.
`to_json()` uses `allow_nan=False` and no fallback serializer.
Unions follow the selected member's scalar contract recursively, including
QName namespaces, list items, numeric values, and non-finite spellings.
Import selects a member using its required JSON representation and XSD
lexical validity; JSON Schema describes the member representations as alternatives.
Native `typed=False` retains lexical text, including the space-separated
string for an XSD list value. `to_dict()` remains the raw-value interface:
it keeps bound Python/XSD scalar objects instead of normalizing them for
JSON. Native JSON keeps its existing structure and decimal/date/QName
strings and Boolean `0`/`1`, with two scalar changes: non-finite numbers use the strings above,
and typed XSD list values use arrays rather than lexical strings.
## Native `pyxsd` structure
The native structure is shared with `to_dict()`:
- The root name is absorbed. A scalar or empty root uses `{"$": value}`.
- Children become properties named with the bound local or Clark name
(`{uri}local`), according to namespace mode.
- One child occurrence is a scalar or object; repeated occurrences with
the same name form an array. `always_list=True` wraps every child value
in an array, but does not create properties for absent children.
- Attributes use `@name`. Text-only elements become scalar values; text
alongside attributes or children uses `$`.
- Direct mixed text and child tails are concatenated under `$`. Their
positions relative to children are lost. Grouping repeated children also
loses interleaving such as `a, b, a`.
A typed empty complex leaf without attributes or children becomes `None`,
as can a nilled leaf without attributes. Native `null` therefore does not
uniquely identify nil. An actual `xsi:nil` attribute remains an `@` property;
for example, a nilled empty child can be
`{"empty": {"@xsi:nil": "true"}}` in legacy namespace mode, rather than
`{"empty": null}`. Use `pyxsd-json` for a dedicated nil marker.
## Schema-driven `pyxsd-json` structure
This is profile 1. The root is always a single-entry object. Every
namespaced element or attribute uses an EQName: `Q{uri}local`; an
unnamespaced name stays `local`. Attributes add `@`, for example
`@Q{urn:example}id`. Namespace declarations and source prefixes are not data.
QName scalar values also use EQNames; on export, unresolved QName namespace
context raises `namespace-unresolved`, even in best-effort mode. Prefer the default
namespace-aware binding; see {doc}`binding` for namespace modes.
The effective bound type selects each layout, including types selected by
substitution or `xsi:type`. Observed child counts never select the layout.
An optional attribute declared by the type still selects an attribute-capable
layout when absent; an attribute wildcard does too.
### Empty and simple content
| Effective content | Without attributes | With declared attributes or an attribute wildcard |
| --- | --- | --- |
| Empty | `{"root": ""}` | `{"root": {"@id": "r"}}` |
| Simple | `{"root": "text"}` | `{"root": {"@id": "r", "#content": "text"}}` |
These are the `empty`, `empty-plus`, `simple`, and `simple-plus` layouts.
Simple content follows the scalar policy, so it can also be a number,
boolean, or XSD list array. Absent attributes are omitted: empty-plus with
no actual attributes is `{}`, while simple-plus still has `#content`.
Declared attributes use bound typed values; wildcard attributes retain
lexical strings. Synthesized attribute defaults are not added to the actual
attribute map.
### Repeated children: list and list-plus
An element-only type with one possible repeatable child name and no
wildcards uses `list` without attributes, or `list-plus` with attributes.
Computed maximum cardinality must exceed one. A list item is the child's
complete converted value, including its attributes and nested content.
```json
{"dates": ["2026-01-01", "2026-02-01"]}
```
List-plus keeps the child name as an array property:
```json
{"dates": {"@source": "known", "date": ["2026-01-01"]}}
```
Both arrays stay arrays with one occurrence. With none, list is `[]` and
list-plus still includes `"date": []`. A repeated-child layout is distinct
from an XSD list-valued scalar; nested arrays are possible.
### Unordered children: record
A closed, wildcard-free element-only model uses `record` when sibling
order is insignificant and every possible child name occurs at most once.
`xs:all` is the principal case:
```json
{"person": {"@id": "p1", "name": "Ada", "age": 37}}
```
Optional absent children are omitted. Attributes precede child properties;
children follow schema declaration order. Object property order carries no
XML ordering semantics here.
### Ordered children: sequence
Other element-only models use `sequence`, including ordered models with
unique child names, choices with possible repetition, and element wildcards
or open content. A nonempty attribute map comes first, followed by a
singleton child map for every occurrence:
```json
{"section": [
{"@id": "s1"},
{"head": "Introduction"},
{"p": "First"},
{"note": "Aside"},
{"p": "Second"}
]}
```
The array preserves bound child order and interleaving. Whitespace-only
layout text is discarded; non-whitespace element-only sequence text raises
`container-invalid`.
### Mixed content
Mixed content uses an array with an optional leading attribute map,
direct-text strings, and singleton child maps. Each child's tail follows
that child, with whitespace preserved:
```json
{"para": [{"@id": "p1"}, "This is a ", {"i": "fine"}, " mess!"]}
```
This retains recorded text/element positions, not source text-node
segmentation, comments, or original namespace prefixes. It is not a
source-exact XML representation.
### Explicit actual types
When an actual `xsi:type` attribute selects a bound type different from the
declared type, export adds the reserved `@xsi:type` key with an EQName value:
```json
{"root": {"@xsi:type": "Q{http://www.w3.org/2001/XMLSchema}int", "#content": 5}}
```
A redundant `xsi:type` equal to the declared type is omitted. The marker
promotes empty, simple, or list content to its plus layout. For sequence and
mixed layouts it belongs in the leading attribute map.
### Nil, empty, and absent
In `pyxsd-json`, `null` means nil, not absence or empty content. `xsi:nil`
is represented by the layout's content marker, never an ordinary attribute:
| Layout | Nilled value, inside the root object |
| --- | --- |
| Empty, simple, list | `null` |
| Empty-plus, simple-plus, list-plus, record | `{"@id": "r", "#content": null}` (attributes only if present) |
| Sequence, mixed | `[{"@id": "r"}, {"#content": null}]` (attribute map only if nonempty) |
Nilled object layouts omit child properties, including the list-plus child
array. Nilled arrays contain only the optional attribute map and nil marker.
An absent child has no property or array entry. An empty string is `""`;
an empty repeated-child container is `[]`; an empty record or attribute
object is `{}`. Defaults already applied by binding are exported as bound
values, not as their original empty lexical input.
## Parker structure
Parker absorbs the root without a `$` wrapper: a simple root is its scalar
value, a nilled root is `null`, and empty element-only content is `{}`.
Children use local-name properties. Only actual repeats become arrays;
one occurrence stays a scalar or object even when the schema permits more.
Absent children have no property.
```json
{"date": ["2026-01-01", "2026-02-01"]}
```
Parker omits attributes and namespace declarations. It can retain
unambiguous local names, but not namespace-dependent QName values or
ambiguous names. Strict mode rejects those losses; best effort warns and
uses the fallbacks below. Text-only mixed content becomes a scalar. With
children, best effort concatenates leading text and child tails under `$`,
losing their positions:
```json
{"$": "This is a mess!", "i": "fine"}
```
Empty simple string content is `""`, an empty XSD list value is `[]`, and
nil is `null`; absence means an omitted child property. Parker's nil marker
does not require keeping the source `xsi:nil` attribute.
## BadgerFish structure
BadgerFish retains the root as a single-entry object. Attributes use `@`
plus a lexical name. A simple element without attributes is a scalar;
with attributes, its text uses `$`:
```json
{"root": {"@id": "r", "$": "text"}}
```
Children become lexical-name properties. A schema-repeatable child uses an
array whenever present, even for one occurrence. Absent children, including
repeatable ones, have no property. Actual repeats of a non-repeatable name
also become an array rather than discarding values.
```json
{"dates": {"date": ["2026-01-01"]}}
```
Empty complex content is an attribute object, or `{}` without attributes.
Empty simple string content is `""`; an empty XSD list value is `[]`.
Nil uses `"@xsi:nil": true`, not `null` or `$`, and retains other attributes
but no text or children:
```json
{"root": {
"@xmlns": {"xsi": "http://www.w3.org/2001/XMLSchema-instance"},
"@xsi:nil": true,
"@id": "r"
}}
```
An explicit actual type uses `@xsi:type` with a lexical QName value; a
redundant type equal to the declared type is omitted. Namespace declarations
use `@xmlns` on the root value. If that value would be a scalar or XSD list
array, declarations require wrapping it under `$`:
```json
{"ns0:root": {"@xmlns": {"ns0": "urn:example"}, "$": "x"}}
```
Mixed text with children and unreconstructable child order use the same
grouping fallbacks as Parker, with warnings in best-effort mode. BadgerFish
does not add an ordered-array extension.
## JsonML structure
JsonML returns an array, not a root object: `[tag, attributes?, ...content]`.
Child elements are nested JsonML arrays. Attribute names have no `@` prefix;
the attribute object is omitted when empty. Repetition never changes this
shape: every child occurrence has its own array, and absent children have
no entry.
```json
["para", {"id": "p1"}, "This is a ", ["i", "fine"], " mess!"]
```
Direct text, children, and child tails retain their bound positions. Mixed
whitespace is preserved; whitespace-only element-only layout text is
discarded. Like `pyxsd-json`, this retains bound content, not source text-node
segmentation, comments, or original prefixes.
Simple content uses the common typed scalar policy. **Typed content items
are a pyxsd extension to JsonML**, not a claim that lexical-only JsonML
consumers accept them. Numbers and booleans remain typed, decimals remain
strings, and an XSD list value occupies one array-valued content item:
```json
["root", ["a", "b"]]
```
Here the schema identifies the content as an XSD list, not a child tag `a`.
Consumers must understand this schema-typed extension to distinguish list
values from nested element arrays; the array shape alone is insufficient.
Empty simple string content is `["root", ""]`; empty complex content is
`["root"]`. Nil adds one `null` content item after any attributes, with no
other content: `["root", {"id": "r"}, null]`. It does not add `xsi:nil`.
An explicit actual type uses an `xsi:type` attribute with a lexical QName.
## Lexical namespaces in BadgerFish and JsonML
Both formats synthesize deterministic lexical QNames rather than preserving
source prefixes. Export and JSON Schema generation use the same compiled-schema
closure of declared types, conditional alternatives, and `xsi:type` target
types. Namespace URIs reachable through that closure receive sorted `ns0`,
`ns1`, … prefixes, including reservations for absent declarations and unselected
types; numbering does not depend on which types an instance uses.
Wildcard-only names and QName-value-only namespaces use stable
`ns_` prefixes. Unnamespaced names stay local; `xsi` is declared
when used, while `xml` is implicitly bound.
Only used bindings are emitted, sorted by prefix, once at the root for the
whole subtree. BadgerFish uses `@xmlns`; JsonML adds `xmlns:prefix` keys to
the root attribute object:
```json
["ns0:root", {"xmlns:ns0": "urn:example"}, ["ns0:child", "v"]]
```
Export uses explicit prefixes, not a default namespace (`$` inside a
BadgerFish `@xmlns` map). QName values and `xsi:type` reuse these bindings,
including namespaces used only in values. An unresolved QName raises
`namespace-unresolved` in both fidelity modes. Prefer namespace-aware
binding so QName values retain their namespace context.
## Interoperability
Format names do not guarantee identical output across implementations.
The per-format contract tests in `tests/test_data_export.py` compare selected
fixtures with **xmlschema 4.3.2**, a development-only dependency. Those results
are comparison evidence, not normative definitions or proof of general
conformance. The export rules on this page define pyxsd's profiles:
- **Native `pyxsd` and `pyxsd-json`:** xmlschema has no equivalent profiles.
Explicit expected-output fixtures pin native grouped mixed content and
profile 1's ordered sequence arrays instead.
- **Parker:** the tested simple, repeated, ordered, nilled, and local-name
projections agree. Intentional differences include keeping mixed text
under `$` in best effort rather than dropping it, and using `{}` for empty
complex content where xmlschema's tested empty leaf uses `null`.
Matching projections can still emit pyxsd `attributes-lost` warnings where
xmlschema silently drops attributes; strict pyxsd export rejects that loss.
- **BadgerFish:** the recursive fixture agrees, but pyxsd emits simple leaves
directly as typed scalars where xmlschema wraps them under `$`. pyxsd uses
boolean `@xsi:nil` and concatenates mixed text under `$` in best effort;
xmlschema's fixtures use lexical `"true"` and numbered `$1`/`$2` text keys.
- **JsonML:** simple, attributed, repeated, ordered, and recursive fixtures
agree. pyxsd preserves mixed whitespace that xmlschema trims in the tested
fixture, and uses one `null` content item for nil rather than an `xsi:nil`
attribute. Consumers also need the schema-typed extension described above.
In all three conventional formats, pyxsd keeps decimal `1.20` as `"1.20"`
where xmlschema's comparison fixtures emit the number `1.2`. BadgerFish and
JsonML use explicit deterministic prefixes with root-scoped declarations;
the namespaced comparison fixtures use a default namespace in xmlschema.
Check these scalar, nil, namespace, and mixed-content choices before passing
exports to another implementation. Strict fidelity does not switch profiles
or guarantee that another implementation accepts the result.
## Ordering and fidelity
Export is deterministic for the same bound tree. In `pyxsd-json`, repeated,
sequence, and mixed arrays follow document order. Object layouts emit an
explicit type marker first, then declared attributes in declaration order,
then sorted wildcard attributes; record children follow schema order.
Sequence/mixed attribute maps sort their keys instead, because attribute
order is not semantic.
The following fidelity rules describe **export**; see
[import fidelity](#import-fidelity) for reconstruction fallbacks.
`fidelity` concerns representation, not XSD validity:
- Native `best-effort` keeps the existing grouped shape without new warnings.
- Native `strict` raises `PyXSDError` for empty/nil ambiguity
(`empty-nil-ambiguous`), mixed text/element positions (`mixed-content`),
interleaved repeated names (`order-ambiguous`), and namespaced elements
reduced to local names by legacy binding (`namespace-lost`). Namespace-dependent
or unresolved QName values in content, lists, attributes, or selected union
members also raise `namespace-lost`: native data carries no bindings for them.
An explicit
nil attribute is representable and need not raise.
- `pyxsd-json` defines no best-effort fallback. Both modes return the same
representable data and raise for content incompatible with the selected
layout, such as unexpected attributes on a scalar, repeated record
children, or a nilled element carrying content. They do not silently drop
that content to fit the layout.
- Parker and BadgerFish raise `PyXSDError` for the losses below in strict
mode. Best effort applies the listed fallback and emits `PyXSDWarning`
for each reported loss.
- JsonML defines no representational-loss fallback: both modes preserve
ordered/mixed content and return the same data without loss warnings.
| Format | Loss code | Best-effort fallback |
| --- | --- | --- |
| Parker | `attributes-lost` | Omit actual ordinary attributes and any required explicit type marker. |
| Parker | `namespace-lost` | Keep a namespace-dependent or unresolved QName's lexical spelling without its context. |
| Parker | `name-ambiguous` | Keep the local child key; colliding values share an array rather than selecting a declaration. |
| Parker | `root-ambiguous` | Keep root absorption without resolving which concrete global root shares the local name. |
| Parker, BadgerFish | `mixed-content` | Group children and concatenate leading text and tails under `$`. |
| Parker, BadgerFish | `order-ambiguous` | Group children in schema order, then wildcard extras in first-observed order; retain occurrence order within each group. |
The grouped-order audit detects interleaved repeats (`a, b, a`), child
order that differs from the schema's reconstructable order, and mixed
content with more than one distinct child name. Ordered unique children
that follow schema order need not raise. Text-only mixed content does not
lose positions and need not raise either.
Parker, BadgerFish, and JsonML reject incompatible bound containers with
`container-invalid` in either mode: for example, children in simple/empty
content, non-whitespace text in element-only content, or content on a
nilled element. Best effort is not permission to silently discard invalid
content. BadgerFish and JsonML export actual attributes with bound typed
values where available, otherwise lexical strings; binding-synthesized
attribute defaults are not added to the actual attribute map.
Conversion errors carry reason codes such as `container-invalid` and a
JSON Pointer path (RFC 6901); loss warnings carry them too. The empty pointer
denotes the whole value.
JsonML diagnostics are relative to the output array, not an XML tag path.
A root container error uses `""`; a root attribute named `ref` uses `/1/ref`.
Content starts at `/1` without an attribute object or `/2` with one, including
an object added solely for root namespace declarations. A child at `/1` has
its own attribute at `/1/1/ref`; its tag name is not a pointer segment.
Object-key tokens escape `~` as `~0` and `/` as `~1`.
Unknown format or fidelity names and inapplicable options raise `ValueError`.
[parker]: https://wiki.open311.org/JSON_and_XML_Conversion/#the-parker-convention
[badgerfish]: http://www.sklar.com/badgerfish/
[jsonml]: https://jsonml.org/