Set a custom property in a word processing document

Custom properties are stored in docProps/custom.xml, separate from the main document body and extended application properties.

Each custom property stores a name, a property id (pid), a fixed format id (fmtid), and exactly one typed value element from the document property value namespace.

Custom property markup

<property fmtid="{D5CDD505-2E9C-101B-9397-08002B2CF9AE}" name="Reviewed" pid="2">
  <vt:bool>true</vt:bool>
</property>

Common value element names include vt:lpwstr for strings, vt:filetime for timestamps, integer value elements, floating-point value elements, and vt:bool for booleans.

Rust workflow

Custom properties are written through a package-level CustomFilePropertiesPart. This tested listing creates the part when it is absent, writes one string property, and saves the package:

#![allow(unused)]
fn main() {
pub fn set_custom_string_property(
  path: &Path,
  name: &str,
  value: &str,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
  let mut document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?;
  let custom_properties_part = match document.custom_file_properties_part() {
    Some(part) => part,
    None => document.add_custom_file_properties_part()?,
  };
  let name = escape_xml_text(name);
  let value = escape_xml_text(value);
  let xml = format!(
    r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Properties xmlns="http://schemas.openxmlformats.org/officeDocument/2006/custom-properties" xmlns:vt="http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes">
  <property fmtid="{{D5CDD505-2E9C-101B-9397-08002B2CF9AE}}" pid="2" name="{name}">
    <vt:lpwstr>{value}</vt:lpwstr>
  </property>
</Properties>"#
  );

  custom_properties_part.set_data(&mut document, xml.into_bytes())?;

  let mut buffer = Cursor::new(Vec::new());
  document.save(&mut buffer)?;
  Ok(buffer.into_inner())
}
}

Application properties are separate from custom properties and are read through the extended properties part:

#![allow(unused)]
fn main() {
pub fn get_application_properties(
  path: &Path,
) -> Result<Vec<(String, String)>, Box<dyn std::error::Error>> {
  let document = WordprocessingDocument::new_from_file_with_settings(path, lazy_settings())?;
  let Some(app_part) = document.extended_file_properties_part() else {
    return Ok(Vec::new());
  };
  let xml = app_part.data_as_str(&document)?.unwrap_or_default();

  Ok(extract_known_app_properties(xml))
}
}

The listing is deliberately narrow: it writes a single vt:lpwstr value. A full custom-property updater should preserve unrelated properties, allocate unique pid values, choose the correct value element for each type, and replace an existing property by name without duplicating it.

When updating an existing property, replacing the whole property element is often simpler than mutating the old value because the value element name encodes the property type. After insertion or replacement, keep pid values unique and stable for the saved part.