Skip to content

Custom Properties

Custom properties are name and value pairs an author or a program attaches to a document. Unlike the built-in properties, the names are yours: “Reviewer”, “ContractID”, “ExpiryDate”. Word shows them under Custom in the advanced properties dialog and can insert them into the body as DOCPROPERTY fields, which makes them a lightweight way to carry structured data with a document.

They live in docProps/custom.xml, a part that many documents simply do not have. Document.GetOrCreateCustomProperties creates the part, its content type override and its relationship on first use, so it is the call to reach for whether the document already has properties or not.

Every value is typed, and the type is part of the name of the setter you call.

SetterGo typeStored as
SetPropertyAsLpwstrstringvt:lpwstr
SetPropertyAsI4int32vt:i4
SetPropertyAsR8float64vt:r8
SetPropertyAsBoolboolvt:bool
SetPropertyAsDatetime.Timevt:filetime

Those five cover nearly everything. SetPropertyAsLpwstr is the right one for text, since lpwstr is the Unicode string Word writes; SetPropertyAsLpstr exists but produces the narrow variant. SetPropertyAsDate deliberately stores a vt:filetime rather than a vt:date, because Word does not display vt:date at all. The full set covers the rest of the variant types, including SetPropertyAsVector and SetPropertyAsArray for compound values.

Reading and writing

doc, err := document.Open("document.docx")
if err != nil {
    log.Fatalf("error opening document: %s", err)
}
defer doc.Close()

cp := doc.GetOrCreateCustomProperties()

if p := cp.GetPropertyByName("Company"); p.X() != nil {
    fmt.Println("Company:", *p.X().PropertyChoice.Lpwstr)
}

cp.SetPropertyAsLpwstr("Reviewer", "Jane Smith")
cp.SetPropertyAsDate("Reviewed", time.Now())

doc.SaveToFile("document_customized.docx")

Reading is where this API asks the most of you. GetPropertyByName returns a CustomProperty, and the value hangs off X().PropertyChoice as one non-nil pointer among the variant fields, so you have to know which type was stored and pick the matching field. There is no AsString() or similar.

PropertiesList() returns every property as []*custom_properties.CT_Property when you want to enumerate rather than look up, which is also the way to discover what types a document actually uses.

Setting a name that already exists replaces its value in place; setting a new one appends. The same call does both, so there is no add-or-update decision to make.

Nothing about this is specific to documents opened from disk. On a document from document.New() the first GetOrCreateCustomProperties creates the part and the rest is identical.

Limitations

GetPropertyByName returns a zero-value CustomProperty when the name is not found, and its X() is nil. The dereference chain that reads a value therefore panics on a missing name rather than returning an error, which is why the guard above checks X() != nil first. The example prints the zero value for a deliberately absent name to show what it looks like.

Names match exactly, including case. “Company” and “company” are two different properties.

CustomProperty, the type GetPropertyByName returns, is marked deprecated in its own doc comment while remaining the only way to read a single property by name. Treat the deprecation as a signal that the read side may change, not as a reason to avoid it.

Replacing an existing property renumbers it. The replacement is written with a fresh pid one past the highest in use rather than keeping the property’s own, so the document ends up with a gap in the pid sequence. Word tolerates this and the property continues to work, but a document round-tripped repeatedly will drift its pid values upward.

SetPropertyAsDate converts to UTC and truncates to whole seconds before writing, on the grounds that local times and fractional seconds both upset Office. SetPropertyAsFiletime writes what you give it, if you need the value preserved exactly.

Run the example

The example reads four properties out of document.docx, prints the zero value returned for a name that does not exist, replaces Company, adds four new properties of four different types and saves to document_customized.docx. It then does the same on a document from document.New() to show that the flow does not differ.

git clone https://github.com/unidoc/unioffice-examples.git
cd unioffice-examples/document/doc-custom-properties
go run main.go

If this is your first time using UniOffice, follow the getting started guide to create an API key and set up your development environment.

View the full source

Sample output

AppVersion 00.500
Company Unidoc
DocSecurity 0
LinksUpToDate false
Non-existent {<nil>}
Company Another company

{<nil>} is the zero CustomProperty printed for the missing name. Reading a value off it would have panicked.

Last updated on