diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index 8820bccd6..91b06d199 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -4,6 +4,7 @@
 
 RELEASE 9.1.7 - June 2021
   * (GH-26) Fixed AppHdr JSON conversion with explicit new namespace field as discriminator
+  * (GH-24) Added a new MxWriteConfiguration and EscapeHandler API to tweak the serialization into XML
 
 RELEASE 9.1.6 - April 2021
   * (GH-17|JIRA-506) Enhanced the XML format in the serializing, spaces and line breaks
diff --git a/build.gradle b/build.gradle
index bfd4ffa2d..ac5f12e5f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -224,7 +224,7 @@ task writePom {
 }
 
 dependencies {
-	// included build (keep in sync with the latest Prowide Core version)
+	// included build (keep in sync with the latest Prowide Core version, this goes to Maven transitive dependency)
 	api 'com.prowidesoftware:pw-swift-core:SRU2020-9.1.4'
 	implementation 'org.apache.commons:commons-lang3:3.8.1'
 	implementation 'com.google.code.gson:gson:2.8.2'
diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/AbstractMX.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/AbstractMX.java
index 2c4bf33ca..7d654ea4a 100644
--- a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/AbstractMX.java
+++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/AbstractMX.java
@@ -43,6 +43,7 @@
 import javax.xml.transform.dom.DOMResult;
 import javax.xml.transform.stream.StreamSource;
 import java.io.*;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.logging.Level;
@@ -66,14 +67,17 @@
  * @since 7.6
  */
 public abstract class AbstractMX extends AbstractMessage implements IDocument, JsonSerializable {
-    public static final String DOCUMENT_LOCALNAME = "Document";
     private static final transient Logger log = Logger.getLogger(AbstractMX.class.getName());
+
+    public static final String DOCUMENT_LOCALNAME = "Document";
+
     /**
      * Default root element when an MX is serialized as XML including both AppHdr and Document
      *
      * @since 8.0.2
      */
     public static String DEFAULT_ROOT_ELEMENT = "RequestPayload";
+
     /**
      * Header portion of the message payload, common to all specific MX subclasses.
      * This information is required before opening the actual message to process the content properly.
@@ -93,8 +97,20 @@ protected AbstractMX(final AppHdr appHdr) {
         this.appHdr = appHdr;
     }
 
+    /**
+     * @deprecated use {@link #message(String, AbstractMX, Class[], String, boolean, EscapeHandler)} instead
+     */
+    @Deprecated
+    @ProwideDeprecated(phase2 = TargetYear.SRU2022)
     protected static String message(final String namespace, final AbstractMX obj, @SuppressWarnings("rawtypes") final Class[] classes, final String prefix, boolean includeXMLDeclaration) {
-        return MxWriteImpl.write(namespace, obj, classes, prefix, includeXMLDeclaration);
+        return message(namespace, obj, classes, prefix, includeXMLDeclaration, null);
+    }
+
+    /**
+     * @since 9.1.7
+     */
+    protected static String message(final String namespace, final AbstractMX obj, @SuppressWarnings("rawtypes") final Class[] classes, final String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) {
+        return MxWriteImpl.write(namespace, obj, classes, prefix, includeXMLDeclaration, escapeHandler);
     }
 
     @SuppressWarnings({"rawtypes", "unchecked"})
@@ -193,7 +209,7 @@ protected static <T> T fromJson(String json, Class<T> classOfT) {
         final Gson gson = new GsonBuilder()
                 .registerTypeAdapter(AbstractMX.class, new AbstractMXAdapter())
                 .registerTypeAdapter(XMLGregorianCalendar.class, new XMLGregorianCalendarAdapter())
-				.registerTypeAdapter(AppHdr.class, new AppHdrAdapter())
+                .registerTypeAdapter(AppHdr.class, new AppHdrAdapter())
                 .create();
         return gson.fromJson(json, classOfT);
     }
@@ -209,7 +225,7 @@ public static AbstractMX fromJson(String json) {
         final Gson gson = new GsonBuilder()
                 .registerTypeAdapter(AbstractMX.class, new AbstractMXAdapter())
                 .registerTypeAdapter(XMLGregorianCalendar.class, new XMLGregorianCalendarAdapter())
-				.registerTypeAdapter(AppHdr.class, new AppHdrAdapter())
+                .registerTypeAdapter(AppHdr.class, new AppHdrAdapter())
                 .create();
         return gson.fromJson(json, AbstractMX.class);
     }
@@ -263,76 +279,99 @@ public static AbstractMX fromJson(String json) {
 
     /**
      * Get this message as an XML string.
+     *
      * <p>If the header is present, then 'AppHdr' and 'Document' elements will be wrapped under a
-     * {@link #DEFAULT_ROOT_ELEMENT}
-     * <br>Both header and documents are generated with the corresponding namespaces and by default the prefix 'h' is
-     * used for the header and the prefix 'Doc' for the document.
+     * {@link #DEFAULT_ROOT_ELEMENT}. Both header and document are generated with the corresponding namespaces and by
+     * default the prefix 'h' is used for the header and the prefix 'Doc' for the document.
+     * <br>For more serialization options see {@link #message(MxWriteConfiguration)}
+     * <br>To serialize only the header or the document (without header) see {@link #header()} and {@link #document()}
      *
-     * @see #message(String, boolean)
+     * @return the XML content or null if errors occur during serialization
      * @since 7.7
      */
     @Override
     public String message() {
-        return message(null, true);
+        return message((MxWriteConfiguration) null);
+    }
+
+    /**
+     * @deprecated use {@link #message(MxWriteConfiguration)} instead
+     */
+    @Deprecated
+    @ProwideDeprecated(phase2 = TargetYear.SRU2022)
+    public String message(final String rootElement, boolean includeXMLDeclaration) {
+        MxWriteConfiguration conf = new MxWriteConfiguration();
+        conf.rootElement = rootElement;
+        conf.includeXMLDeclaration = includeXMLDeclaration;
+        return message(conf);
+    }
+
+    /**
+     * @deprecated use {@link #message(MxWriteConfiguration)} instead
+     */
+    @Deprecated
+    @ProwideDeprecated(phase2 = TargetYear.SRU2022)
+    public String message(final String rootElement) {
+        MxWriteConfiguration conf = new MxWriteConfiguration();
+        conf.rootElement = rootElement;
+        return message(conf);
     }
 
     /**
      * Get this message as an XML string.
      *
      * <p>If the business header is set, the created XML will include both the 'AppHdr' and the 'Document' elements,
-     * under a the indicated or default root element.
-     * <br>If the header is not present, the created XMl will only include the 'Document'.
-     * <br>Both 'AppHdr' and 'Document' are generated with namespace declaration and default prefixes 'h' and 'Doc'
-     * respectively.
+     * under a the indicated or default root element. If the header is not present, the created XMl will only include
+     * the 'Document'. Both 'AppHdr' and 'Document' are generated with namespace declaration and if optional prefixes
+     * if present in the configuration.
      *
      * <p>IMPORTANT: The name of the envelope element that binds a Header to the message to which it applies is
      * implementation/network specific. The header root element ‘AppHdr’ and the ISO 20022 MessageDefinition
      * root element ‘Document’ must always be sibling elements in any XML document, with the AppHdr element preceding
      * the Document element.
      *
-     * @param rootElement           optional specification of the root element if not provided {@link #DEFAULT_ROOT_ELEMENT} is used
-     * @param includeXMLDeclaration true to include the XML declaration
-     * @return header serialized into XML string or null if the header is not set or errors occur during serialization
-     * @return created XML
-     * @since 7.8
+     * @param conf specific options for the serialization or null to use the default parameters
+     * @return the XML content or null if errors occur during serialization
      */
-    public String message(final String rootElement, boolean includeXMLDeclaration) {
-        String root = rootElement != null ? rootElement : DEFAULT_ROOT_ELEMENT;
+    public String message(MxWriteConfiguration conf) {
+        MxWriteConfiguration usableConf = conf != null? conf : new MxWriteConfiguration();
+        String root = usableConf.rootElement;
         StringBuilder xml = new StringBuilder();
-        if (includeXMLDeclaration) {
+        if (usableConf.includeXMLDeclaration) {
             xml.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
         }
-        final String header = header("h", false);
+        final String header = header(usableConf.headerPrefix, false, usableConf.escapeHandler);
         if (header != null) {
-            xml.append("<" + root + ">\n");
-            xml.append(header + "\n");
+            xml.append("<").append(root).append(">\n");
+            xml.append(header).append("\n");
         }
-        xml.append(document("Doc", false) + "\n");
+        xml.append(document(usableConf.documentPrefix, false, usableConf.escapeHandler)).append("\n");
         if (header != null) {
-            xml.append("</" + root + ">");
+            xml.append("</").append(root).append(">");
         }
         return xml.toString();
     }
 
     /**
-     * Same as {@link #message(String, boolean)} with includeXMLDeclaration set to true
+     * Get this message AppHdr as an XML string.
      *
+     * <p>The XML will not include the XML declaration, will bind the namespace to all elements without prefix and will
+     * use the default escape handler. For more serialization options use {@link #header(String, boolean, EscapeHandler)}
+     *
+     * @return the serialized header or null if header is not set or errors occur during serialization
      * @since 7.8
      */
-    public String message(final String rootElement) {
-        return message(rootElement, true);
+    public String header() {
+        return header(null, false, null);
     }
 
     /**
-     * Get this message AppHdr as an XML string.
-     * <p>The XML will not include the XML declaration, and will include de namespace as default (without prefix).
-     *
-     * @return the serialized header or null if header is not set or errors occur during serialization
-     * @see #header(String, boolean)
-     * @since 7.8
+     * @deprecated use {@link #header(String, boolean, EscapeHandler)} instead
      */
-    public String header() {
-        return header(null, false);
+    @Deprecated
+    @ProwideDeprecated(phase2 = TargetYear.SRU2022)
+    public String header(final String prefix, boolean includeXMLDeclaration) {
+        return header(prefix, includeXMLDeclaration, null);
     }
 
     /**
@@ -340,12 +379,13 @@ public String header() {
      *
      * @param prefix                optional prefix for namespace (empty by default)
      * @param includeXMLDeclaration true to include the XML declaration
+     * @param escapeHandler         a specific escape handler for the header elements content
      * @return header serialized into XML string or null if the header is not set or errors occur during serialization
-     * @since 7.8
+     * @since 9.1.7
      */
-    public String header(final String prefix, boolean includeXMLDeclaration) {
+    public String header(final String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) {
         if (this.appHdr != null) {
-            return this.appHdr.xml(prefix, includeXMLDeclaration);
+            return this.appHdr.xml(prefix, includeXMLDeclaration, escapeHandler);
         } else {
             return null;
         }
@@ -353,26 +393,38 @@ public String header(final String prefix, boolean includeXMLDeclaration) {
 
     /**
      * Get this message Document as an XML string.
-     * <p>The XML will include the XML declaration, and will use "Doc" as prefix for the elements.
+     *
+     * <p>The XML will not include the XML declaration, will bind the namespace to all elements using "Doc" as default
+     * prefix and will use the default escape handler. For more serialization options use
+     * {@link #document(String, boolean, EscapeHandler)}
      *
      * @return document serialized into XML string or null if errors occur during serialization
-     * @see #document(String, boolean)
      * @since 7.8
      */
     public String document() {
         return document("Doc", true);
     }
 
+    /**
+     * @deprecated use {@link #document(String, boolean, EscapeHandler)} instead
+     */
+    @Deprecated
+    @ProwideDeprecated(phase2 = TargetYear.SRU2022)
+    public String document(final String prefix, boolean includeXMLDeclaration) {
+        return document(prefix, includeXMLDeclaration, null);
+    }
+
     /**
      * Get this message Document as an XML string.
      *
      * @param prefix                optional prefix for namespace (empty by default)
      * @param includeXMLDeclaration true to include the XML declaration
+     * @param escapeHandler         a specific escape handler for the document elements content
      * @return document serialized into XML string or null if errors occur during serialization
-     * @since 7.8
+     * @since 9.1.7
      */
-    public String document(final String prefix, boolean includeXMLDeclaration) {
-        return message(getNamespace(), this, getClasses(), prefix, includeXMLDeclaration);
+    public String document(final String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) {
+        return message(getNamespace(), this, getClasses(), prefix, includeXMLDeclaration, escapeHandler);
     }
 
     /**
@@ -392,7 +444,7 @@ public Source xmlSource() {
     }
 
     /**
-     * Writes the message document content into a file in XML format (headers not included).
+     * Writes the message content into a file in XML format.
      *
      * @param file a not null file to write, if it does not exists, it will be created
      * @since 7.7
@@ -417,7 +469,7 @@ public void write(final File file) throws IOException {
      */
     public void write(final OutputStream stream) throws IOException {
         Validate.notNull(stream, "the stream to write cannot be null");
-        stream.write(message().getBytes("UTF-8"));
+        stream.write(message().getBytes(StandardCharsets.UTF_8));
     }
 
     /**
@@ -521,7 +573,7 @@ public String toJson() {
         final Gson gson = new GsonBuilder()
                 .registerTypeAdapter(AbstractMX.class, new AbstractMXAdapter())
                 .registerTypeAdapter(XMLGregorianCalendar.class, new XMLGregorianCalendarAdapter())
-				.registerTypeAdapter(AppHdr.class, new AppHdrAdapter())
+                .registerTypeAdapter(AppHdr.class, new AppHdrAdapter())
                 .setPrettyPrinting()
                 .create();
         // we use AbstractMX and not this.getClass() in order to force usage of the adapter
diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/AppHdr.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/AppHdr.java
index 3fba81e41..73de87a83 100644
--- a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/AppHdr.java
+++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/AppHdr.java
@@ -114,7 +114,7 @@ public interface AppHdr {
      * Get this header as an XML string.
      * <p>The implementation uses {@link #xml(String, boolean)} with no prefix and no XML declaration.
      *
-     * @return header serialized into XML string or null if neither header version is present
+     * @return header serialized into XML string or null in case of unexpected error
      */
     default String xml() {
         return xml(null, false);
@@ -125,9 +125,23 @@ default String xml() {
      *
      * @param prefix                optional prefix for namespace (empty by default)
      * @param includeXMLDeclaration true to include the XML declaration (false by default)
+     * @return header serialized into XML string or null in case of unexpected error
      */
     String xml(final String prefix, boolean includeXMLDeclaration);
 
+    /**
+     * Get this header as an XML string.
+     *
+     * @param prefix                optional prefix for namespace (empty by default)
+     * @param includeXMLDeclaration true to include the XML declaration (false by default)
+     * @param escapeHandler         a specific escape handler for the header elements content
+     * @return header serialized into XML string or null in case of unexpected error
+     * @since 9.1.7
+     */
+    default String xml(final String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) {
+        return xml(prefix, includeXMLDeclaration);
+    }
+
     /**
      * Gets the header as an Element object.
      */
@@ -135,6 +149,7 @@ default String xml() {
 
     /**
      * Gets the specific namespace of the header
+     *
      * @return default implementation returns null
      * @since 9.1.7
      */
diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/BusinessAppHdrV01.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/BusinessAppHdrV01.java
index 96171d432..e20b1fe39 100644
--- a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/BusinessAppHdrV01.java
+++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/BusinessAppHdrV01.java
@@ -185,14 +185,20 @@ public void setCreationDate(boolean overwrite) {
     }
 
     @Override
-    public String xml(String prefix, boolean includeXMLDeclaration) {
+    public String xml(final String prefix, boolean includeXMLDeclaration) {
+        return xml(prefix, includeXMLDeclaration, null);
+    }
+
+    @Override
+    public String xml(String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) {
         try {
             JAXBContext context = JAXBContext.newInstance(BusinessApplicationHeaderV01Impl.class);
             final Marshaller marshaller = context.createMarshaller();
 
             final StringWriter sw = new StringWriter();
             JAXBElement<BusinessApplicationHeaderV01Impl> element = new JAXBElement(new QName(NAMESPACE, AppHdr.HEADER_LOCALNAME), BusinessApplicationHeaderV01Impl.class, null, this);
-            marshaller.marshal(element, new XmlEventWriter(sw, prefix, includeXMLDeclaration, AppHdr.HEADER_LOCALNAME));
+            XmlEventWriter eventWriter = new XmlEventWriter(sw, prefix, includeXMLDeclaration, AppHdr.HEADER_LOCALNAME, escapeHandler);
+            marshaller.marshal(element, eventWriter);
             return sw.getBuffer().toString();
 
         } catch (JAXBException e) {
diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/BusinessAppHdrV02.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/BusinessAppHdrV02.java
index 8b5ccbd24..ca77a39ca 100644
--- a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/BusinessAppHdrV02.java
+++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/BusinessAppHdrV02.java
@@ -186,13 +186,19 @@ public void setCreationDate(boolean overwrite) {
 
     @Override
     public String xml(String prefix, boolean includeXMLDeclaration) {
+        return xml(prefix, includeXMLDeclaration, null);
+    }
+
+    @Override
+    public String xml(String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) {
         try {
             JAXBContext context = JAXBContext.newInstance(BusinessApplicationHeaderV02Impl.class);
             final Marshaller marshaller = context.createMarshaller();
 
             final StringWriter sw = new StringWriter();
             JAXBElement<BusinessApplicationHeaderV02Impl> element = new JAXBElement(new QName(NAMESPACE, AppHdr.HEADER_LOCALNAME), BusinessApplicationHeaderV02Impl.class, null, this);
-            marshaller.marshal(element, new XmlEventWriter(sw, prefix, includeXMLDeclaration, AppHdr.HEADER_LOCALNAME));
+            XmlEventWriter eventWriter = new XmlEventWriter(sw, prefix, includeXMLDeclaration, AppHdr.HEADER_LOCALNAME, escapeHandler);
+            marshaller.marshal(element, eventWriter);
             return sw.getBuffer().toString();
 
         } catch (JAXBException e) {
diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/DefaultEscapeHandler.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/DefaultEscapeHandler.java
new file mode 100644
index 000000000..5221b5037
--- /dev/null
+++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/DefaultEscapeHandler.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2006-2021 Prowide
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.prowidesoftware.swift.model.mx;
+
+/**
+ * Escapes &amp;, &lt;, &gt;, quotes (in attribute) and everything above the US-ASCII code range.
+ * Similar to com.sun.xml.bind.marshaller.DumbEscapeHandler or apache.commons.text.StringEscapeUtils#escapeXml
+ *
+ * @since 9.1.7
+ */
+public class DefaultEscapeHandler implements EscapeHandler {
+
+    @Override
+    public String escape(char[] arr, boolean isAttribute) {
+        final StringBuilder sb = new StringBuilder(arr.length);
+        for (int i = 0; i < arr.length; i++) {
+            switch (arr[i]) {
+                case '&':
+                    sb.append("&amp;");
+                    break;
+                case '<':
+                    sb.append("&lt;");
+                    break;
+                case '>':
+                    sb.append("&gt;");
+                    break;
+                case '\"':
+                    if (isAttribute) {
+                        sb.append("&quot;");
+                    } else {
+                        sb.append('\"');
+                    }
+                    sb.append('\"');
+                    break;
+                default:
+                    if (arr[i] > '\u007f') {
+                        sb.append("&#");
+                        sb.append(Integer.toString(arr[i]));
+                        sb.append(';');
+                    } else {
+                        sb.append(arr[i]);
+                    }
+            }
+        }
+        return sb.toString();
+    }
+
+}
diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/EscapeHandler.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/EscapeHandler.java
new file mode 100644
index 000000000..d22e9aa38
--- /dev/null
+++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/EscapeHandler.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2006-2021 Prowide
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.prowidesoftware.swift.model.mx;
+
+/**
+ * Simple interface used by the XML event writer when serializing the XML element content
+ * @since 9.1.7
+ */
+public interface EscapeHandler {
+
+    /**
+     * @param arr the characters to escape
+     * @param isAttribute true if it is an attribute value
+     */
+    String escape(char[] arr, boolean isAttribute);
+
+}
diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/LegacyAppHdr.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/LegacyAppHdr.java
index 9e6002735..5e478249b 100644
--- a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/LegacyAppHdr.java
+++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/LegacyAppHdr.java
@@ -175,14 +175,20 @@ public void setCreationDate(boolean overwrite) {
     }
 
     @Override
-    public String xml(String prefix, boolean includeXMLDeclaration) {
+    public String xml(final String prefix, boolean includeXMLDeclaration) {
+        return xml(prefix, includeXMLDeclaration, null);
+    }
+
+    @Override
+    public String xml(String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) {
         try {
             JAXBContext context = JAXBContext.newInstance(ApplicationHeaderImpl.class);
             final Marshaller marshaller = context.createMarshaller();
 
             final StringWriter sw = new StringWriter();
             JAXBElement<ApplicationHeaderImpl> element = new JAXBElement(new QName(NAMESPACE, AppHdr.HEADER_LOCALNAME), ApplicationHeaderImpl.class, null, this);
-            marshaller.marshal(element, new XmlEventWriter(sw, prefix, includeXMLDeclaration, AppHdr.HEADER_LOCALNAME));
+            XmlEventWriter eventWriter = new XmlEventWriter(sw, prefix, includeXMLDeclaration, AppHdr.HEADER_LOCALNAME, escapeHandler);
+            marshaller.marshal(element, eventWriter);
             return sw.getBuffer().toString();
 
         } catch (JAXBException e) {
diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MinimumEscapeHandler.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MinimumEscapeHandler.java
new file mode 100644
index 000000000..cd1813dcb
--- /dev/null
+++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MinimumEscapeHandler.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2006-2021 Prowide
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.prowidesoftware.swift.model.mx;
+
+/**
+ * Only escapes &amp;, &lt;, &gt; and quotes (in attribute).
+ * Similar to com.sun.xml.bind.marshaller.MinimumEscapeHandler
+ *
+ * @since 9.1.7
+ */
+public class MinimumEscapeHandler implements EscapeHandler {
+
+    @Override
+    public String escape(char[] arr, boolean isAttribute) {
+        final StringBuilder sb = new StringBuilder(arr.length);
+        for (int i = 0; i < arr.length; i++) {
+            switch (arr[i]) {
+                case '&':
+                    sb.append("&amp;");
+                    break;
+                case '<':
+                    sb.append("&lt;");
+                    break;
+                case '>':
+                    sb.append("&gt;");
+                    break;
+                case '\"':
+                    if (isAttribute) {
+                        sb.append("&quot;");
+                    } else {
+                        sb.append('\"');
+                    }
+                    sb.append('\"');
+                    break;
+                default:
+                    sb.append(arr[i]);
+            }
+        }
+        return sb.toString();
+    }
+
+}
diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWrite.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWrite.java
index 1d9d49d4c..54d152e63 100644
--- a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWrite.java
+++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWrite.java
@@ -15,6 +15,9 @@
  */
 package com.prowidesoftware.swift.model.mx;
 
+import com.prowidesoftware.deprecation.ProwideDeprecated;
+import com.prowidesoftware.deprecation.TargetYear;
+
 /**
  * Interface to plug in code that serializes MX message objects to XML string
  *
@@ -22,6 +25,13 @@
  */
 public interface MxWrite {
 
+    /**
+     * @deprecated use {@link #message(String, AbstractMX, Class[], String, boolean, EscapeHandler)} instead
+     */
+    @Deprecated
+    @ProwideDeprecated(phase2 = TargetYear.SRU2022)
+    String message(String namespace, AbstractMX obj, Class[] classes, final String prefix, boolean includeXMLDeclaration);
+
     /**
      * Converts obj into a xml string
      *
@@ -30,9 +40,12 @@ public interface MxWrite {
      * @param classes               array of all classes used or referenced by message class
      * @param prefix                optional prefix for ns ("Doc" by default)
      * @param includeXMLDeclaration true to include the xml declaration (true by default)
+     * @param escapeHandler specific escape handler to use when serializing the elements content or null to use the default
      * @return the message content serialized to XML
-     * @since 7.8
+     * @since 9.1.7
      */
-    String message(String namespace, AbstractMX obj, Class[] classes, final String prefix, boolean includeXMLDeclaration);
+    default String message(String namespace, AbstractMX obj, Class[] classes, final String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) {
+        return message(namespace, obj, classes, prefix, includeXMLDeclaration, null);
+    }
 
 }
diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWriteConfiguration.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWriteConfiguration.java
new file mode 100644
index 000000000..b663eb10d
--- /dev/null
+++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWriteConfiguration.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2006-2021 Prowide
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.prowidesoftware.swift.model.mx;
+
+/**
+ * Options POJO to customize the behaviour of the MX writer (model into XML serialization)
+ *
+ * @since 9.1.7
+ */
+public class MxWriteConfiguration {
+
+    /**
+     * The name of the envelope element that binds a Header to the message to which it applies is
+     * implementation/network specific. The header root element ‘AppHdr’ and the ISO 20022 MessageDefinition
+     * root element ‘Document’ must always be sibling elements in any XML document, with the AppHdr element preceding
+     * the Document element. If no root elemewnt name is provided the value in {@link AbstractMX#DEFAULT_ROOT_ELEMENT}
+     * is used as default
+     */
+    public String rootElement = AbstractMX.DEFAULT_ROOT_ELEMENT;
+
+    /**
+     * Determines if the XML will include the XML declaration as first line. It is true by default. You can switch this
+     * off if the generated XML will then be used a a fragment of another XML wrapper.
+     */
+    public boolean includeXMLDeclaration = true;
+
+    /**
+     * Enables switching between different implementations for the element and attributes value escaping. Some
+     * implementations are available in the library and your own custom class can also be used. This is useful if you
+     * handle XML messages with specific charset and you want to control what is escaped and what is propagated as is.
+     */
+    public EscapeHandler escapeHandler = new DefaultEscapeHandler();
+
+    /**
+     * The prefix for the header namespace. Set it to null if you don't want to have any prefix in header elements.
+     * It is "h" by default.
+     */
+    public String headerPrefix = "h";
+
+    /**
+     * The prefix for the document namespace. Set it to null if you don't want to have any prefix in document elements.
+     * It is "Doc" by default.
+     */
+    public String documentPrefix = "Doc";
+}
diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWriteImpl.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWriteImpl.java
index 757ae17a7..d1e5964f6 100644
--- a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWriteImpl.java
+++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/MxWriteImpl.java
@@ -15,6 +15,8 @@
  */
 package com.prowidesoftware.swift.model.mx;
 
+import com.prowidesoftware.deprecation.ProwideDeprecated;
+import com.prowidesoftware.deprecation.TargetYear;
 import com.prowidesoftware.swift.model.MxSwiftMessage;
 import org.apache.commons.lang3.Validate;
 
@@ -40,11 +42,20 @@ public class MxWriteImpl implements MxWrite {
     private static final transient Logger log = Logger.getLogger(MxWriteImpl.class.getName());
 
     /**
-     * Static serialization implementation of {@link MxWrite#message(String, AbstractMX, Class[], String, boolean)}
-     *
-     * @since 9.0
+     * @deprecated use {@link #message(String, AbstractMX, Class[], String, boolean, EscapeHandler)} instead
      */
+    @Deprecated
+    @ProwideDeprecated(phase2 = TargetYear.SRU2022)
     public static String write(String namespace, AbstractMX obj, Class[] classes, final String prefix, boolean includeXMLDeclaration) {
+        return write(namespace, obj, classes, prefix, includeXMLDeclaration, null);
+    }
+
+    /**
+     * Static serialization implementation of {@link MxWrite#message(String, AbstractMX, Class[], String, boolean, EscapeHandler)}
+     *
+     * @since 9.1.7
+     */
+    public static String write(String namespace, AbstractMX obj, Class[] classes, final String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) {
         Validate.notNull(namespace, "namespace can not be null");
         Validate.notNull(obj, "MxSwiftMessage can not be null");
         Validate.notNull(classes, "Class[] can not be null");
@@ -58,7 +69,7 @@ public static String write(String namespace, AbstractMX obj, Class[] classes, fi
             final Marshaller marshaller = context.createMarshaller();
 
             final StringWriter sw = new StringWriter();
-            XmlEventWriter writer = new XmlEventWriter(sw, prefix, includeXMLDeclaration, "Document");
+            XmlEventWriter writer = new XmlEventWriter(sw, prefix, includeXMLDeclaration, "Document", escapeHandler);
 
             Map<String, String> preferredPrefixes = new HashMap<>();
             for (XsysNamespaces xsys : XsysNamespaces.values()) {
@@ -79,14 +90,24 @@ public static String write(String namespace, AbstractMX obj, Class[] classes, fi
         return null;
     }
 
+    /**
+     * @deprecated use {@link #message(String, AbstractMX, Class[], String, boolean, EscapeHandler)} instead
+     */
+    @Deprecated
+    @ProwideDeprecated(phase2 = TargetYear.SRU2022)
+    @Override
+    public String message(String namespace, AbstractMX obj, Class[] classes, final String prefix, boolean includeXMLDeclaration) {
+        return write(namespace, obj, classes, prefix, includeXMLDeclaration, null);
+    }
+
     /**
      * Implements serialization to XML
      *
-     * @see MxWrite#message(String, AbstractMX, Class[], String, boolean)
+     * @see MxWrite#message(String, AbstractMX, Class[], String, boolean, EscapeHandler)
      */
     @Override
-    public String message(String namespace, AbstractMX obj, Class[] classes, final String prefix, boolean includeXMLDeclaration) {
-        return write(namespace, obj, classes, prefix, includeXMLDeclaration);
+    public String message(String namespace, AbstractMX obj, Class[] classes, final String prefix, boolean includeXMLDeclaration, EscapeHandler escapeHandler) {
+        return write(namespace, obj, classes, prefix, includeXMLDeclaration, escapeHandler);
     }
 
 }
diff --git a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/XmlEventWriter.java b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/XmlEventWriter.java
index a852f7f05..b9a550a62 100644
--- a/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/XmlEventWriter.java
+++ b/iso20022-core/src/main/java/com/prowidesoftware/swift/model/mx/XmlEventWriter.java
@@ -53,21 +53,34 @@ public final class XmlEventWriter implements XMLEventWriter {
     private boolean preserveQnamePrefixes = false;
     private int previousNestedStartLevel;
     private XMLEvent previousEvent;
+    private EscapeHandler escapeHandler;
+
+    /**
+     * @deprecated use {@link #XmlEventWriter(Writer, String, boolean, String, EscapeHandler)} instead
+     */
+    @Deprecated
+    @ProwideDeprecated(phase2 = TargetYear.SRU2022)
+    public XmlEventWriter(Writer baos, final String defaultPrefix, boolean includeXMLDeclaration, final String rootElement) {
+        this(baos, defaultPrefix, includeXMLDeclaration, rootElement, null);
+    }
 
     /**
      * @param baos                  output buffer to write
      * @param defaultPrefix         optional prefix (empty by default) to used for all elements that are not binded to a specific prefix
      * @param includeXMLDeclaration true to include the XML declaration (true by default)
      * @param rootElement           local name of the root element of the XML fragment to create, used to declare namespace
+     * @param escapeHandler         escape handler to use or null to use the default
      * @see #setPreferredPrefixes(Map)
+     * @since 9.1.7
      */
-    public XmlEventWriter(Writer baos, final String defaultPrefix, boolean includeXMLDeclaration, final String rootElement) {
+    public XmlEventWriter(Writer baos, final String defaultPrefix, boolean includeXMLDeclaration, final String rootElement, final EscapeHandler escapeHandler) {
         this.out = baos;
         this.startElementCount = 0;
         this.nestedLevel = 0;
         this.defaultPrefix = defaultPrefix;
         this.includeXMLDeclaration = includeXMLDeclaration;
         this.rootElement = rootElement;
+        this.escapeHandler = escapeHandler != null ? escapeHandler : new DefaultEscapeHandler();
     }
 
     public void add(final XMLEvent event) throws XMLStreamException {
@@ -141,7 +154,8 @@ public void add(final XMLEvent event) throws XMLStreamException {
                             break;
                         }
                         final char[] arr = ce.getData().toCharArray();
-                        out.write(escape(arr));
+                        String escapedString = this.escapeHandler.escape(arr, false);
+                        out.write(escapedString);
                         this.previousEvent = event;
                         break;
                     }
@@ -187,7 +201,8 @@ public void add(final XMLEvent event) throws XMLStreamException {
 
                     case XMLEvent.ATTRIBUTE: {
                         final Attribute a = (Attribute) event;
-                        out.write(" " + a.getName() + "=\"" + a.getValue() + "\"");
+                        String escapedString = a.getValue() != null ? this.escapeHandler.escape(a.getValue().toCharArray(), true) : "";
+                        out.write(" " + a.getName() + "=\"" + escapedString + "\"");
                         this.previousEvent = event;
                         break;
                     }
@@ -231,41 +246,6 @@ private String namespace(final Namespace namespace) {
         return sb.toString();
     }
 
-    /**
-     * Inplace escape por xml
-     *
-     * @since 7.8
-     */
-    private String escape(char[] arr) {
-        final StringBuilder sb = new StringBuilder(arr.length);
-        // TODO Consider code in com.sun.xml.bind.marshaller.DumbEscapeHandler for replacements
-        for (int i = 0; i < arr.length; i++) {
-            switch (arr[i]) {
-                case '&':
-                    sb.append("&amp;");
-                    break;
-                case '<':
-                    sb.append("&lt;");
-                    break;
-                case '>':
-                    sb.append("&gt;");
-                    break;
-                case '\"':
-                    sb.append('\"');
-                    break;
-                default:
-                    if (arr[i] > '\u007f') {
-                        sb.append("&#");
-                        sb.append(Integer.toString(arr[i]));
-                        sb.append(';');
-                    } else {
-                        sb.append(arr[i]);
-                    }
-            }
-        }
-        return sb.toString();
-    }
-
     private String prefixString(final QName qname) {
         String prefix = resolvePrefix(qname);
         if (prefix != null) {
diff --git a/iso20022-core/src/test/java/com/prowidesoftware/issues/Issue24.java b/iso20022-core/src/test/java/com/prowidesoftware/issues/Issue24.java
new file mode 100644
index 000000000..2591cb541
--- /dev/null
+++ b/iso20022-core/src/test/java/com/prowidesoftware/issues/Issue24.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 2006-2021 Prowide
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.prowidesoftware.issues;
+
+import com.prowidesoftware.swift.model.mx.MinimumEscapeHandler;
+import com.prowidesoftware.swift.model.mx.MxPain00100103;
+import com.prowidesoftware.swift.model.mx.MxWriteConfiguration;
+import com.prowidesoftware.swift.model.mx.dic.CustomerCreditTransferInitiationV03;
+import com.prowidesoftware.swift.model.mx.dic.GroupHeader32;
+import com.prowidesoftware.swift.model.mx.dic.PartyIdentification32;
+import com.prowidesoftware.swift.model.mx.dic.PaymentInstructionInformation3;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * https://github.com/prowide/prowide-iso20022/issues/24
+ */
+public class Issue24 {
+
+    private MxPain00100103 sample() {
+        MxPain00100103 mx = new MxPain00100103();
+        mx.setCstmrCdtTrfInitn(new CustomerCreditTransferInitiationV03());
+        mx.getCstmrCdtTrfInitn().setGrpHdr(new GroupHeader32());
+        mx.getCstmrCdtTrfInitn().getGrpHdr().setMsgId("1234");
+        mx.getCstmrCdtTrfInitn().addPmtInf(new PaymentInstructionInformation3());
+        mx.getCstmrCdtTrfInitn().getPmtInf().get(0).setDbtr(new PartyIdentification32());
+        mx.getCstmrCdtTrfInitn().getPmtInf().get(0).getDbtr().setNm("текст текст öñ");
+        return mx;
+    }
+
+    @Test
+    public void testWriteDefaultEscapeHandler() {
+        MxPain00100103 mx = sample();
+        String xml = mx.message();
+        assertTrue(xml.contains("&#1090;&#1077;&#1082;&#1089;&#1090; &#1090;&#1077;&#1082;&#1089;&#1090; &#246;&#241;"));
+
+        MxPain00100103 mx2 = MxPain00100103.parse(xml);
+        assertEquals("текст текст öñ", mx2.getCstmrCdtTrfInitn().getPmtInf().get(0).getDbtr().getNm());
+        assertEquals(mx, mx2);
+    }
+
+    @Test
+    public void testWriteDefaultMinimumEscapeHandler() {
+        MxPain00100103 mx = sample();
+
+        MxWriteConfiguration conf = new MxWriteConfiguration();
+        conf.escapeHandler = new MinimumEscapeHandler();
+        String xml = mx.message(conf);
+
+        assertTrue(xml.contains("текст текст öñ"));
+
+        MxPain00100103 mx2 = MxPain00100103.parse(xml);
+        assertEquals("текст текст öñ", mx2.getCstmrCdtTrfInitn().getPmtInf().get(0).getDbtr().getNm());
+        assertEquals(mx, mx2);
+    }
+
+    @Test
+    public void testParse() {
+        String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n" +
+                "<Doc:Document xmlns:Doc=\"urn:iso:std:iso:20022:tech:xsd:pain.001.001.03\">\n" +
+                "    <Doc:CstmrCdtTrfInitn>\n" +
+                "        <Doc:GrpHdr>\n" +
+                "            <Doc:MsgId>1234</Doc:MsgId>\n" +
+                "        </Doc:GrpHdr>\n" +
+                "        <Doc:PmtInf>\n" +
+                "            <Doc:Dbtr>\n" +
+                "                <Doc:Nm>текст текст öñ</Doc:Nm>\n" +
+                "            </Doc:Dbtr>\n" +
+                "        </Doc:PmtInf>\n" +
+                "    </Doc:CstmrCdtTrfInitn>\n" +
+                "</Doc:Document>";
+        MxPain00100103 mx2 = MxPain00100103.parse(xml);
+        assertEquals("текст текст öñ", mx2.getCstmrCdtTrfInitn().getPmtInf().get(0).getDbtr().getNm());
+    }
+
+}