A recurring payment notification failed its signature check, and nothing in the flat fields was wrong. The difference was one element that contained other elements.
This came out of bluemedia, our open-source client for a payment gateway's ITN notifications. Most of that notification is flat: a tag, some text, sign it in the declared order. recurringData is not flat. It holds a recurring action and an optional client hash.
Our first instinct was to treat it as a special case, one branch in the parser and another in the signer. That is where the bug lived. Two branches drift.
What we do now is model the nested element as a document of its own. The recurring data is a full model with its own field order, and the notification just declares a nested field that remembers the specification name and the model class and defaults to None.
Parsing became a single walk over the children. An undeclared tag is an error. A repeated tag is an error. A child that itself holds elements is an error unless its declaration names a model, and when it does, the value is that model parsed from the child. Empty text counts as absent. A required element that never arrives raises before anything downstream can read a half-built object.
Signing follows the same idea instead of a parallel one. The nested model contributes its ordered values as a list under its specification name, and flattening expands that list exactly where recurringData sits in the notification's field order, at the end. So the recurring action and the client hash sign one after the other, in the only place they could.
The payoff is the quiet case. A notification with no recurring data has None there, nothing expands, and the string being hashed is identical to what it was before the feature existed. Writing back works the same way: the nested element gets appended as an element instead of having text set on it.
The general lesson we carry into other integrations: when a field's shape is different, that is probably a signal to make a smaller model, not a bigger branch.
Where you draw that line is the interesting part, the point at which a nested structure earns its own model instead of a special case in the code that reads it.