Aim
Reading this means, more than likely, you are familiar with the planned changes to the use of addresses in ISO 20022 payment messages for CBPR+, BoE CHAPS, SEPA and other payment schemes and market infrastructures. If you need refreshing, refer to ISO 20022 Structured Addresses.
With the adoption of structured address formats, capturing this more complex data from your users, in your payments screens or app, has the potential to be a poor customer experience. A structured address has 141 individual data fields and the use of each varies by country. When dealing predominantly with domestic payments, to creditors in the same country as the debtor, this may not be of concern however, when dealing with cross-border payments these variances will be more apparent and a different approach, simplifying the user experience, will be required.
Many user interfaces may already be applying address validation and geo-correction services and whilst this may alleviant some issues, you may still find this article useful.
In this article we aim to provide a concept for dynamically configuring these 14 fields by selected country which, when incorporated into your user interface, should help to simplify the user input. Coupled with address validation and geo-correction, the experience should be significantly improved.
This concept is based on the PMPG structured postal address spreadsheet and will show how to:
Step 1. Produce a structured address configuration file (xml) from the PMPG’s ISO 20022 Structured Postal Address – Country Guidance
Step 2. Add a default country configuration (for countries not listed).
Step 3. Use the configuration file in a sample user interface to dynamically configure the 14 individual address fields depending on the country selected.
It will also include additional configuration parameters, such as field length and permitted characters, which can be extended further for specific field validation. These allow you to restrict input for specific fields, ensuring the resulting payment message conforms to the relevant usage guidelines.
Finally, whilst you’ll see the xsd covers purpose codes and regulatory reporting information, we won’t discuss those here, rather in future, dedicated articles.
Dynamic Address Configuration
The final artefact from this article will be a user interface which dynamically changes each address control based on a selected country. In our concept and in the screenshots below, we will change the properties of each control, such as the background colour and watermark text, as prescribed by the configuration file. This could be amended to suit your UI principles, such as to hide or disable controls or apply any other type of validation.
In Figure 1, our demo UI, having selected Austria as the country, the individual address controls are configured according to the PMPG spreadsheet; Country and Town Name both mandatory, are painted pink with the associated watermark text. Fields that are expected, are painted green (Street Name, Building Number and Post Code), the remainder are optional. Fields that are not used, would be greyed out. Each time the selected country changes, the configuration for that country is applied.
Step 1. Build Configuration File
In our demo UI, we’re going to use an xml file to hold the country configurations – so converting the PMPG country guidance spreadsheet into this xml configuration file. Whilst you may want to build your own configuration structure and file, we’ve done this for you. The xsd and completed configuration file are available here:
The xsd for the structure of the configuration file, specifically for the Country/Address, is shown in Figure 2 below.
Its briefly worth explaining how the PMPG Country Guidance spreadsheet has been converted to the xml configuration file. The main principles are shown in Figure 3 and described here:
For each country column in the PMPG spreadsheet, there is an occurrence of <Country> element in the configuration file. The child element <CountryCode> holds the 2 character country code, “AT” for Austria in this example.
Each individual address element, such as Department, is captured in an occurrence of <Element>. IMPORTANT: The child element <Name> will be the name of the control in your UI. In this case we’ve called it <Name>Dept</Name>, which means the name of the control in the UI must be Dept. If your naming convention for UI controls includes the control type, such as “textfieldDept”, this is what you should use for the value of the <Name> element. It is this element that will be used to match to the relevant control in your UI which then applies the values of the elements, <Label>, <Status>, <Length>, <PermittedCharacters> etc, as the configuration.
For each of the individual address elements, which are now in their own occurrence of <Element>, the requirement for the element, such as O (Optional), E (Expected), C (Conditional), M (Mandatory) and N (Not Used), are captured in the <Status> element.
The remaining xml elements are designed for use as follows:
- <Label> You can use the value of this element to change the text of any label associated with this particular address control. For example, for <Name>CtrySubDvsn</Name>, you may want the label associated with this control to show “State” if the country is “US” but show “County” if the country is “GB”. You can use the <Label> value to capture this and apply it to the control’s label in the UI.
- <Length> Use the value of this element to set the maximum length permitted for this control. You can find these values in the relevant usage guidelines (UGs) but our completed configuration file already has these applied for CBPR+ (SR 2025 Grace Period UGs).
- <Occurrences> Used if the UGs permit more than one occurrence of this element. This can be used when rendering the UI controls to create the correct number. Whilst not relevant for a structured address, it is relevant for a hybrid address, which permits 2 occurrences of <AdrLine> (note: this article does not cover hybrid addresses which are intentionally a temporary solution).
- <PermittedCharacters> This is a slightly more complex configuration but one we see as vital in ISO 20022 messages, given the characters that are permitted in each field can vary. This configuration element allows you to apply a custom regex pattern to the specific control to govern the user input. We’ve included an attribute “Set” which can be used for the name of a character set, to apply configuration based on that rather than the specific regex value. For CBPR+ messages, the main character set is FIN X but with certain fields, such as names and address fields, allowing the extended character set, hence Set=”FINXEXTENDED” in these examples. Our demo code/UI uses the regex value itself to apply to the control input, but the Set attribute is included as an alternative method if preferred. A final note about the regex values in this config file; the regex syntax is for a Java implementation and certain characters have to be escaped both for Java syntax, such as \[ and \] , and also due to the constraints of xml, such as & which is escaped as & and < which is escaped as < etc
This structure applies for all of the 30+ countries in the PMPG spreadsheet, each represented in the configuration file as a <Country> element.
Step 2. Add a Default Country Configuration
There are far more countries than currently documented in the PMPG Country Guidance spreadsheet, therefore we need to add a default country configuration. Helpfully, the ISO 3166-1 Alpha 2 standard for country codes provides the code XX for unknown states2. We’ll add this as an occurrence of <Country> in the configuration file and model it on a minimum set of address fields – Mandatory being Country & Town Name. Expected being Street Name and Post Code. All other fields being Optional.
Step 3. Applying Configuration to UI Address Controls
Now we have a comprehensive configuration file, including country code XX for countries without their own configuration, we can integrate this into our UI code. As mentioned earlier, this is a very simple UI demo and doesn’t include any integration with address lookup or geo-correction services.
Our code will:
- Load the xml configuration file.
- Add each UI text field into a HashMap for iteration.
- Create a method for setting the properties for each text field control.
- Extend the method to include field length and permitted characters configuration.
- Add a listener which applies the configuration to each text field each time the selected country is changed.
The application of the xml configuration to the UI is shown in Figure 5 below.
This video explains the xml configuration and shows it applied to the working UI.
Summary
Most banks and payment service providers, through their websites and applications, already include sophisticated address input and validation capabilities, yet the move away from unstructured to (eventually) fully structured addresses may still cause issues. The concepts explained here, combined with other address services, may help this migration journey and crucially, keep things simple for the user.
Future articles will cover extending this configuration to Purpose of Payment Codes and Regulatory Reporting Information but it can be extended yet further, catering for many different use cases and enriched data elements that ISO 20022 now offers.
Appendix: Code
Whilst the code used for our demo UI is not particularly relevant, what you see in the above screenshot was built in JavaFX. For information only, we’ve included key extracts of the code below. If you want further code or information on this, please contact us.
Class to Load Configuration File
A class to load and return the configuration file. There are separate Java classes for the elements of the xml structure – Config.java and its children Country.java, Address.java and AddressElement.java (none shown).
package view;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Unmarshaller;
import xml.Config;
import xml.Country;
import xml.AddressElement;
public class ConfigLoader {
private static final String CONFIG_FILE = "C:\\<your path here>\\ConfigurationByCountry.xml";
public static Map<String, List<AddressElement>> loadConfig() throws JAXBException, IOException {
File configFile = new File(CONFIG_FILE);
if (!configFile.exists()) {
throw new IOException("Configuration file not found: " + configFile.getAbsolutePath());
}
JAXBContext context = JAXBContext.newInstance(Config.class);
Unmarshaller unmarshaller = context.createUnmarshaller();
Config config = (Config) unmarshaller.unmarshal(configFile);
Map<String, List<AddressElement>> countryConfig = new HashMap<>();
for (Country country : config.getCountry()) {
countryConfig.put(country.getCountryCode(), country.getAddress().getElements());
}
return countryConfig;
}
}
Listener for Selected Country Changed
We add a listener to the Country dropdown control, in which the configuration is applied. The listener code is standard and shown here. This invokes the configureTextFields() method passing it the selected country code.
// Add listener for country configuration
Ctry.valueProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
// Handle the selected CountryData object here
String selectedCountryCode = newValue.getCountryCode();
// Set the value of each control in the textFieldMap based on the configuration xml for the selected country code
configureTextFields(selectedCountryCode);
}
});
Configuring the Address Controls
The configuration of each UI control is done in the configureTextFields() method. First though, during initialisation of the screen, each TextField control is added to a HashMap. This is used to iterate through to find the control in the HashMap whose name matched the <Element><Name> value in the loaded configuration.
private Map<String, TextField> textFieldMap = new HashMap<>();
// Populate the textFieldMap with the address controls
textFieldMap.put("Dept", Dept);
textFieldMap.put("SubDept", SubDept);
textFieldMap.put("StrtNm", StrtNm);
textFieldMap.put("BldgNb", BldgNb);
textFieldMap.put("BldgNm", BldgNm);
textFieldMap.put("PstBx", PstBx);
textFieldMap.put("Flr", Flr);
textFieldMap.put("Room", Room);
textFieldMap.put("TwnNm", TwnNm);
textFieldMap.put("PstCd", PstCd);
textFieldMap.put("TwnLctnNm", TwnLctnNm);
textFieldMap.put("DstrctNm", DstrctNm);
textFieldMap.put("CtrySubDvsn", CtrySubDvsn);
private void configureTextFields(String selectedCountryCode) {
List<AddressElement> elements;
// Find the matching country code from the config if it exits for this selected country code
if (!countryConfig.containsKey(selectedCountryCode)) {
elements = countryConfig.get("XX"); // Get the default settings
// Apply configuration for the selected country
}
// else get the settings for this selected country code
else {
elements = countryConfig.get(selectedCountryCode);
}
for (AddressElement element : elements) {
TextField textField = textFieldMap.get(element.getName());
if (textField != null) {
// Set the length
Integer intLength = element.getLength();
// Set the regex for the characters permitted
String regex = element.getPermittedCharacters();
TextFormatter<String> combinedFormatter = new TextFormatter<>(change -> {
change = lengthFilter(change, intLength);
if (change == null) return null;
return regexFilter(change, regex);
});
//textField.setTextFormatter(textFormatter);
textField.setTextFormatter(combinedFormatter);
// Set the prompt text to the status
textField.setPromptText(element.getStatus());
// Configure based on the status
switch (element.getStatus()) {
case "MANDATORY":
textField.setStyle("-fx-background-color: pink");
textField.setDisable(false);
break;
case "EXPECTED":
textField.setStyle("-fx-background-color: lightgreen");
textField.setDisable(false);
break;
case "OPTIONAL":
textField.setStyle("-fx-background-color: white");
textField.setDisable(false);
break;
case "NOTUSED":
textField.setStyle("-fx-background-color: gray");
textField.setDisable(true);
textField.setText(""); // Optional: Clear text
break;
default:
// Handle other statuses or log a warning
System.out.println("Unknown element status: " + element.getStatus());
}
}
}
}
// Length filter function
private static TextFormatter.Change lengthFilter(TextFormatter.Change change, int maxLength) {
if (change.getControlNewText().length() <= maxLength) {
return change;
}
return null;
}
// Regex filter function
private static TextFormatter.Change regexFilter(TextFormatter.Change change, String regex) {
String newText = change.getControlNewText();
if (newText.matches(regex + "*")) {
return change;
} else {
String invalidChars = newText.replaceAll(regex, "");
change.setText(change.getText().replaceAll("[" + Pattern.quote(invalidChars) + "]", ""));
return change;
}
}