7.7.4. PUGIXML-04 — Struct Serialization

This tutorial covers automatic struct ↔ XML serialization using to_XML, from_XML, and the low-level XML() builder in pugixml/PUGIXML_boost.

7.7.4.1. Basic roundtrip

to_XML serializes any struct to an XML string. from_XML parses an XML string back into a struct:

struct Player {
    name : string
    hp : int
    speed : float
    alive : bool
}

let p = Player(name = "Hero", hp = 100, speed = 5.5, alive = true)
let xml_str = to_XML(p)
var restored = from_XML(xml_str, type<Player>)
print("name: {restored.name}, hp: {restored.hp}\n")
// name: Hero, hp: 100

7.7.4.2. Custom root element

By default the root element is <root>. Pass a second argument to override:

let xml_str = to_XML(p, "player")
// <player> ... </player> instead of <root> ... </root>

7.7.4.3. Nested structs

Nested structs serialize as child elements and round-trip correctly:

struct Address {
    street : string
    city : string
    zip : string
}

struct Person {
    name : string
    age : int
    address : Address
}

let p = Person(
    name = "Alice", age = 30,
    address = Address(street = "123 Main St", city = "Springfield", zip = "62704")
)
let xml_str = to_XML(p, "person")
var restored = from_XML(xml_str, type<Person>)
print("restored: {restored.name}, {restored.address.city}\n")
// restored: Alice, Springfield

7.7.4.4. Enums

Enums serialize as their name by default. Use @enum_as_int on a field to serialize as the integer value instead:

enum Weapon {
    sword
    bow
    staff
}

struct Warrior {
    name : string
    weapon : Weapon             // serializes as "sword"
    @enum_as_int backup : Weapon  // serializes as "1"
}

let w = Warrior(name = "Knight", weapon = Weapon.sword, backup = Weapon.bow)
let xml_str = to_XML(w, "warrior")
var restored = from_XML(xml_str, type<Warrior>)
print("weapon: {restored.weapon}, backup: {restored.backup}\n")
// weapon: sword, backup: bow

7.7.4.5. Bitfields

Bitfields serialize as their unsigned integer value:

bitfield Permissions {
    read
    write
    execute
}

let perms = Permissions.read | Permissions.execute
let xml_str = to_XML(perms, "permissions")
var restored = from_XML(xml_str, type<Permissions>)
let has_read = uint(restored & Permissions.read) != 0u
print("read: {has_read}\n")
// read: true

7.7.4.6. Arrays and tables

array<T> elements become <item> children. table<K;V> entries become <entry> children with <_key> and <_val>:

struct Inventory {
    items : array<string>
    counts : table<string; int>
}

var inv <- Inventory(
    items <- ["sword", "shield", "potion"],
    counts <- { "sword" => 1, "shield" => 2, "potion" => 5 }
)
let xml_str = to_XML(inv, "inventory")
var restored <- from_XML(xml_str, type<Inventory>)
print("items: {length(restored.items)}\n")
// items: 3

7.7.4.7. Tuples and variants

Tuples serialize with _0, _1, … field names. Variants store the active index in a _variant attribute:

variant Shape {
    circle : float
    rectangle : float2
}

// Tuple roundtrip
let t = (42, "hello", 3.14)
let t_xml = to_XML(t, "tuple")
var t_back = from_XML(t_xml, type<tuple<int; string; float>>)
print("restored: ({t_back._0}, {t_back._1}, {t_back._2})\n")
// restored: (42, hello, 3.14)

// Variant roundtrip
let s = Shape(circle = 5.0)
let s_xml = to_XML(s, "shape")
var s_back = from_XML(s_xml, type<Shape>)
print("restored circle: {s_back as circle}\n")
// restored circle: 5

7.7.4.8. Fixed-size arrays (dim)

Fixed-size arrays serialize as sequential <item> children:

struct Matrix2x2 {
    data : int[4]
}

var m = Matrix2x2(data = fixed_array(1, 0, 0, 1))
let xml_str = to_XML(m, "matrix")
var restored = from_XML(xml_str, type<Matrix2x2>)
print("restored: [{restored.data[0]}, {restored.data[1]}, {restored.data[2]}, {restored.data[3]}]\n")
// restored: [1, 0, 0, 1]

7.7.4.9. Vector types

float2/3/4 and int2/3/4 serialize with x, y, z, w child elements:

let pos = float3(1.0, 2.5, -3.0)
let xml_str = to_XML(pos, "position")
var restored = from_XML(xml_str, type<float3>)
print("restored: ({restored.x}, {restored.y}, {restored.z})\n")
// restored: (1, 2.5, -3)

7.7.4.10. The @rename annotation

Use @rename = "xml_name" to change the XML element name for a field without changing the daslang field name:

struct Config {
    @rename = "type" _type : string
    @enum_as_int level : Priority
    name : string
}

7.7.4.11. The low-level XML() builder

XML() serializes a struct’s fields into an existing xml_node. from_XML(node, type<T>) deserializes from a node. This gives you control over the document structure — e.g. embedding multiple records:

with_doc() <| $(doc) {
    doc |> tag("records") <| $(var records) {
        let p1 = Player(name = "Alice", hp = 100, speed = 5.0, alive = true)
        let p2 = Player(name = "Bob", hp = 80, speed = 3.5, alive = false)

        records |> tag("player") <| $(var node) {
            XML(node, p1)
        }
        records |> tag("player") <| $(var node) {
            XML(node, p2)
        }
    }

    // Read them back
    let records = doc.document_element
    records |> for_each_child("player") <| $(node) {
        var p = from_XML(node, type<Player>)
        print("  {p.name}: hp={p.hp}\n")
    }
}

7.7.4.12. Complete roundtrip

A full game-state roundtrip with nested struct, array, and vector:

struct GameState {
    level : int
    score : int
    player : Player
    weapons : array<string>
    position : float3
}

var state <- GameState(
    level = 3, score = 42000,
    player = Player(name = "Hero", hp = 95, speed = 6.0, alive = true),
    weapons <- ["sword", "bow", "fireball"],
    position = float3(10.5, 0.0, -3.2)
)
let xml_str = to_XML(state, "save")
var loaded <- from_XML(xml_str, type<GameState>)
print("Level: {loaded.level}, Score: {loaded.score}\n")
print("Player: {loaded.player.name}, HP: {loaded.player.hp}\n")
print("Weapons: {length(loaded.weapons)}\n")