| title | Rust |
|---|---|
| description | Using PolyXML's native Rust engine directly in Rust applications for high-throughput, zero-allocation XML processing. |
The polyxml crate is the pure, idiomatic Rust core engine driving the entire PolyXML ecosystem. It provides ultra-fast streaming serialization and deserialization with zero FFI overhead, zero C dependencies, and no intermediate DOM allocations.
Add polyxml to your Cargo.toml:
[dependencies]
polyxml = "0.1"PolyXML distinguishes between XML attributes, child elements, and text content through FieldKind.
use std::sync::Arc;
use polyxml::schema::{FieldKind, FieldSchema, ModelSchema, ScalarType, ValueType};
use polyxml::{deserialize, serialize};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// 1. Define the schema programmatically
let schema = ModelSchema::builder("Telemetry")
.field(FieldSchema::new("device_id", b"id", FieldKind::Attribute, ValueType::Scalar(ScalarType::Int)).required())
.field(FieldSchema::new("altitude", b"altitude", FieldKind::Element, ValueType::Scalar(ScalarType::Float)))
.field(FieldSchema::new("armed", b"armed", FieldKind::Element, ValueType::Scalar(ScalarType::Bool)))
.field(FieldSchema::new("callsign", b"callsign", FieldKind::Element, ValueType::Scalar(ScalarType::String)))
.build();
let xml = br#"<Telemetry id="402">
<altitude>31500.75</altitude>
<armed>true</armed>
<callsign>GHOST-1</callsign>
</Telemetry>"#;
// 2. Deserialize XML into a PolyValue map
let val = deserialize(xml, Arc::clone(&schema))?;
// 3. Extract strongly-typed scalar values
let device_id = val.get("device_id").and_then(|v| v.as_i64()).unwrap_or(0);
let altitude = val.get("altitude").and_then(|v| v.as_f64()).unwrap_or(0.0);
let armed = val.get("armed").and_then(|v| v.as_bool()).unwrap_or(false);
let callsign = val.get("callsign").and_then(|v| v.as_str()).unwrap_or("");
println!("Telemetry: Device {device_id} ({callsign}) at {altitude}ft, Armed: {armed}");
// 4. Serialize back to formatted XML bytes
let output = serialize("Telemetry", &val, &schema, Some(2))?;
println!("Serialized XML:\n{}", std::str::from_utf8(&output)?);
Ok(())
}PolyXML natively supports complex hierarchical data models, including nested sub-objects (ValueType::Nested) and repeated collections (ValueType::List).
use std::sync::Arc;
use polyxml::schema::{FieldKind, FieldSchema, ModelSchema, ScalarType, ValueType};
use polyxml::{deserialize, serialize, PolyValue};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Define the child schema (Item)
let item_schema = ModelSchema::builder("Item")
.field(FieldSchema::new("sku", b"sku", FieldKind::Attribute, ValueType::Scalar(ScalarType::String)))
.field(FieldSchema::new("qty", b"qty", FieldKind::Element, ValueType::Scalar(ScalarType::Int)))
.field(FieldSchema::new("price", b"price", FieldKind::Element, ValueType::Scalar(ScalarType::Float)))
.build();
// Define the parent schema (Order) containing a list of Items
let order_schema = ModelSchema::builder("Order")
.field(FieldSchema::new("order_id", b"id", FieldKind::Attribute, ValueType::Scalar(ScalarType::Int)))
.field(FieldSchema::new(
"items",
b"Item",
FieldKind::Element,
ValueType::List(Box::new(ValueType::Nested(Arc::clone(&item_schema)))),
))
.build();
let xml = br#"
<Order id="9801">
<Item sku="PART-A"><qty>5</qty><price>19.99</price></Item>
<Item sku="PART-B"><qty>2</qty><price>45.50</price></Item>
</Order>
"#;
let order = deserialize(xml, Arc::clone(&order_schema))?;
// Inspect repeated elements
if let Some(PolyValue::List(items)) = order.get("items") {
for item in items {
let sku = item.get("sku").and_then(|v| v.as_str()).unwrap_or("");
let qty = item.get("qty").and_then(|v| v.as_i64()).unwrap_or(0);
let price = item.get("price").and_then(|v| v.as_f64()).unwrap_or(0.0);
println!("- Item: SKU={sku}, Qty={qty}, Total=${:.2}", (qty as f64) * price);
}
}
Ok(())
}When parsing large XML feeds (e.g. PubMed, Wikipedia, SEC EDGAR, ISO 20022), loading the full document into memory is prohibitive. XmlItemStream yields individual record subtrees, so memory depends on the largest record rather than the complete document.
use std::sync::Arc;
use polyxml::schema::{FieldKind, FieldSchema, ModelSchema, ScalarType, ValueType};
use polyxml::XmlItemStream;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let item_schema = ModelSchema::builder("Product")
.field(FieldSchema::new("id", b"id", FieldKind::Attribute, ValueType::Scalar(ScalarType::Int)))
.field(FieldSchema::new("title", b"title", FieldKind::Element, ValueType::Scalar(ScalarType::String)))
.field(FieldSchema::new("price", b"price", FieldKind::Element, ValueType::Scalar(ScalarType::Float)))
.build();
let large_xml_stream = br#"
<Catalog>
<Product id="1"><title>Sensor Module A</title><price>12.50</price></Product>
<Product id="2"><title>Actuator Hub B</title><price>89.00</price></Product>
<Product id="3"><title>Telemetry Unit C</title><price>245.99</price></Product>
</Catalog>
"#;
// Stream records matching tag "Product" without materializing the <Catalog> DOM
let mut stream = XmlItemStream::new(&large_xml_stream[..], b"Product", Arc::clone(&item_schema));
let mut count = 0;
while let Some(result) = stream.next() {
let product = result?;
let title = product.get("title").and_then(|v| v.as_str()).unwrap_or("");
let price = product.get("price").and_then(|v| v.as_f64()).unwrap_or(0.0);
println!("Streamed product #{}: {} (${:.2})", count + 1, title, price);
count += 1;
}
println!("Total products processed: {count}");
Ok(())
}In production XML feeds, elements may be empty, omitted, or explicitly marked null via XML Schema Instance attributes (xsi:nil="true"). PolyXML natively handles all three scenarios:
use std::sync::Arc;
use polyxml::schema::{FieldKind, FieldSchema, ModelSchema, ScalarType, ValueType};
use polyxml::{deserialize, PolyValue};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let schema = ModelSchema::builder("Record")
.field(FieldSchema::new("id", b"id", FieldKind::Element, ValueType::Scalar(ScalarType::Int)))
.field(FieldSchema::new("notes", b"notes", FieldKind::Element, ValueType::Scalar(ScalarType::String)))
.field(FieldSchema::new("status", b"status", FieldKind::Element, ValueType::Scalar(ScalarType::String)))
.build();
let xml = br#"
<Record xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<id>100</id>
<notes xsi:nil="true"/>
<status></status>
</Record>
"#;
let record = deserialize(xml, schema)?;
// notes is parsed as PolyValue::Null due to xsi:nil="true"
assert_eq!(record.get("notes"), Some(&PolyValue::Null));
// empty <status></status> parses as empty string
assert_eq!(record.get("status").and_then(|v| v.as_str()), Some(""));
Ok(())
}PolyXML automatically unescapes standard XML entities (&, <, >, ", ') as well as numeric character references (A, A), and extracts raw text within <![CDATA[...]]> blocks:
use std::sync::Arc;
use polyxml::schema::{FieldKind, FieldSchema, ModelSchema, ScalarType, ValueType};
use polyxml::deserialize;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let schema = ModelSchema::builder("Payload")
.field(FieldSchema::new("script", b"script", FieldKind::Element, ValueType::Scalar(ScalarType::String)))
.field(FieldSchema::new("desc", b"desc", FieldKind::Element, ValueType::Scalar(ScalarType::String)))
.build();
let xml = br#"
<Payload>
<script><![CDATA[if (a < 10 && b > 20) { return true; }]]></script>
<desc>Tom & Jerry "Classic"</desc>
</Payload>
"#;
let val = deserialize(xml, schema)?;
assert_eq!(val.get("script").unwrap().as_str().unwrap(), "if (a < 10 && b > 20) { return true; }");
assert_eq!(val.get("desc").unwrap().as_str().unwrap(), "Tom & Jerry \"Classic\"");
Ok(())
}polyxml-core natively supports W3C XML namespaces for both serialization and deserialization with zero allocation overhead when disabled.
Declare namespaces on models or specific fields using .with_namespace():
use std::collections::HashMap;
use std::sync::Arc;
use polyxml::schema::{FieldKind, FieldSchema, ModelSchema, ScalarType, ValueType};
use polyxml::{deserialize, serialize_with_options, PolyValue};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Define a schema with model-level and field-level namespaces
let schema = ModelSchema::builder("Order")
.with_namespace("https://example.com/orders")
.field(
FieldSchema::new("id", b"id", FieldKind::Attribute, ValueType::Scalar(ScalarType::Int))
.required(),
)
.field(
FieldSchema::new("item", b"item", FieldKind::Element, ValueType::Scalar(ScalarType::String))
.with_namespace("https://example.com/items"),
)
.build();
let mut record = HashMap::new();
record.insert("id".to_string(), PolyValue::Int(1001));
record.insert("item".to_string(), PolyValue::String("Widget Pro".to_string()));
let val = PolyValue::Object(record);
// 1. Serialization with automatic namespace prefix generation (ns0, ns1, ...)
let bytes = serialize_with_options("Order", &val, &schema, Some(2), true, None)?;
println!("{}", std::str::from_utf8(&bytes)?);
// Output:
// <ns0:Order xmlns:ns0="https://example.com/orders" xmlns:ns1="https://example.com/items" id="1001">
// <ns1:item>Widget Pro</ns1:item>
// </ns0:Order>
// 2. Serialization with custom prefix mapping
let mut ns_map = HashMap::new();
ns_map.insert(Some("ord".to_string()), "https://example.com/orders".to_string());
ns_map.insert(Some("itm".to_string()), "https://example.com/items".to_string());
let bytes = serialize_with_options("Order", &val, &schema, Some(2), true, Some(&ns_map))?;
println!("{}", std::str::from_utf8(&bytes)?);
// Output:
// <ord:Order xmlns:ord="https://example.com/orders" xmlns:itm="https://example.com/items" id="1001">
// <itm:item>Widget Pro</itm:item>
// </ord:Order>
// 3. Deserializing namespaced documents (automatically matches local names and prefixes)
let parsed = deserialize(&bytes, Arc::clone(&schema))?;
assert_eq!(parsed.get("id").and_then(|v| v.as_i64()), Some(1001));
assert_eq!(parsed.get("item").and_then(|v| v.as_str()), Some("Widget Pro"));
Ok(())
}To achieve the maximum throughput from polyxml-core:
- Reuse Schema Instances: Construct
Arc<ModelSchema>once at startup or store it in a staticLazyLock. PassingArc::clone(&schema)is an atomic reference bump that avoids re-indexing fields. - Pre-allocate Buffers: For serialization loops, pass an existing
Vec<u8>or reuse serialization buffers across requests to avoid heap allocations. - Use
XmlItemStreamfor Files > 5 MB: For large XML feeds, streaming ensures your process memory remains constant regardless of file size. - Thread Safety: Both
ModelSchemaandPolyValueare fullySend + Sync, making them ideal for parallel processing withrayonor multi-threaded Tokio runtimes.
Rust models compiled with polyxml generate --lang rust --codecs implement inherent JSON helpers backed by serde_json (borrowing depends on the generated field type and input):
// Generated model from schemas/order.xsd
use generated::rust::Order;
// 1. Serialize to JSON string or Vec<u8>
let json_str: String = order.to_json_string()?;
let json_vec: Vec<u8> = order.to_json_vec()?;
// 2. Deserialize from JSON string slice or byte slice
let restored = Order::from_json_str(&json_str)?;
let from_bytes = Order::from_json_slice(&json_vec)?;To convert a complete document without compiling Rust structs, use
polyxml::transcoder. It accepts byte slices and returns newly allocated
output bytes; schema-free conversion builds an intermediate JSON value tree:
use polyxml::transcoder::{xml_to_json, json_to_xml};
let xml_input = br#"<Product id="42"><name>Sensor</name><price>19.99</price></Product>"#;
// Transcode XML to JSON with pretty formatting
let json_bytes = xml_to_json(
xml_input,
None, // Optional Arc<ModelSchema>
Some(2), // Indentation width
true, // Use schema field aliases
)?;
// Transcode JSON back to XML, inferring the root from its single top-level key
let restored_xml = json_to_xml(
&json_bytes,
None,
None,
Some(2),
None, // Namespace handling
None, // Namespace map
)?;The CLI accepts --feature rkyv, but the generated #[rkyv(check_bytes)] attribute is incompatible with rkyv 0.8. Do not enable this option for new projects until the generator is updated and its output is verified against the rkyv version you use. The default Rust output does not require rkyv.
Generated xs:hexBinary and xs:base64Binary fields preserve their XML lexical
text as Cow<'a, str> (or String with zero-copy disabled), including through
XML round trips. They are not decoded byte buffers. Decode hex/base64 explicitly
when consuming the payload. This matches the core runtime's lexical storage and
avoids passing byte slices to string-based XML codecs.
Download the shared batch schema
as batch.xsd, then generate a module:
polyxml generate batch.xsd --lang rust --out src/generatedThe default output derives serde::Serialize and serde::Deserialize and emits
XML codecs. Add these dependencies to your application's Cargo.toml:
[dependencies]
polyxml = "0.34"
quick-xml = "0.42"
serde = { version = "1", features = ["derive"] }
serde_json = "1"#[path = "generated/batch.rs"]
mod batch;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let xml = r#"<Batch><Sensor><Id>sensor-0</Id><Value>0</Value></Sensor></Batch>"#;
let model = batch::BatchType::from_xml(xml)?;
assert_eq!(model.sensor[0].id, "sensor-0");
// Serde JSON operates on the same generated model.
let json = serde_json::to_string(&model)?;
let restored: batch::BatchType<'_> = serde_json::from_str(&json)?;
assert_eq!(restored.sensor[0].value, 0);
// Specify the XSD element name when it differs from its type name.
let mut writer = quick_xml::Writer::new(Vec::new());
restored.encode_xml(&mut writer, Some("Batch"))?;
let output = writer.into_inner();
batch::BatchType::from_xml_bytes(&output)?;
Ok(())
}Generated XML codecs call quick-xml directly; the Serde derives describe JSON
field names and do not provide an XML mapping for third-party Serde XML crates.
to_xml() uses the generated type's default name, so use encode_xml with an
explicit root as above when the declared element is named differently.
encode_xml also accepts a writer backed by a file or reusable buffer. It writes
an existing model; it does not incrementally construct a repeated-item model.
Borrowed models contain Cow<'a, str> and XML parsing can borrow from the input.
Keep the input alive for the generated API's lifetime; escaping can require
owned strings. The current Serde derives do not emit #[serde(borrow)] on
string fields, so JSON deserialization into those Cow fields allocates owned
strings. Borrowed XML models do not imply allocation-free JSON parsing. For models that
must outlive their input, generate with --zero-copy=false and omit <'_> from the
example's type annotation. JSON helpers such as from_json_str and
to_json_vec are conveniences over the same Serde operations. Disabling XML
codecs with --codecs=false retains the Serde derives.
model.encode_xml(&mut Writer<W>, tag_name) writes an existing model to a
sink, but its repeated fields are already materialized in Vecs. to_xml()
also retains the final document bytes. For a root containing one repeated
element in a plain sequence, generation additionally exposes an associated
write_<root>_items(&mut sink, iterator) function. It needs neither a root
instance nor a collected child list.
For example, this schema declares a required sequence of items:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:complexType name="ItemType">
<xs:sequence><xs:element name="Name" type="xs:string"/></xs:sequence>
</xs:complexType>
<xs:complexType name="BatchType">
<xs:sequence>
<xs:element name="Item" type="ItemType" minOccurs="1" maxOccurs="unbounded"/>
</xs:sequence>
</xs:complexType>
<xs:element name="Batch" type="BatchType"/>
</xs:schema>Generate Rust models using polyxml generate items.xsd --lang rust --out src.
The lazy iterator below creates one item at a time:
use items::{Batch, ItemType};
use std::io::{BufWriter, Write};
let mut sink = BufWriter::new(std::fs::File::create("batch.xml")?);
let items = (0..1_000_000).map(|i| ItemType {
name: format!("Item {i}").into(),
});
Batch::write_batch_items(&mut sink, items)?;
sink.flush()?;The producer writes the declared root name and namespace, escapes item text,
and preserves each item's field order through its generated codec. It checks
minOccurs at iterator exhaustion and maxOccurs before writing an excess
item. An excess item is consumed to detect the error. Sink errors propagate;
a failed call may leave partial XML. The caller owns buffering, flushing, and
publishing the output file.
This API is generated for named, concrete sequence types with exactly one repeated child, no root attributes, and element namespaces matching the root throughout the item types. Unqualified documents and uniformly qualified documents with local unqualified attributes are supported. Choices, repeated or optional groups, inheritance, references, wildcards, recursion, nillable values, named simple restrictions, and qualified attributes are excluded. Existing model codecs remain available for those schemas. The producer checks the root collection's occurrence bounds; it is not a complete per-item XSD validator. Python iterable production is not exposed by this API.
See the memory study for the compiled example, independent XSD validation, and reproducible peak RSS measurements.