Repository: powsybl/powsybl-core
Pull request #3282: Different extension's serialization names depending on the version
Author: login: olperr1
Labels: name: Release 03/2025
Created: 2025-01-21T16:34:02Z | Merged: 2025-03-06T16:23:07Z

**Please check if the PR fulfills these requirements**
<!-- please use `'[x]'` to check the checkboxes, or submit the PR and then click the checkboxes -->
- [X] The commit message follows our guidelines
- [X] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)


**Does this PR already have an issue describing the problem?**
<!-- If so, link to this issue using `'Fixes #XXX'` and skip the rest -->
No


**What kind of change does this PR introduce?**
<!-- Bug fix, feature, docs update, ... -->
Feature


**What is the current behavior?**
<!-- You can also link to an open issue here -->



**What is the new behavior (if this is a feature change)?**
Add several improvements on the extension serialization:
- It is now possible to use another serialization name and/or XML namespace prefix when exporting an extension.
  - When an alternative serialization name for an extension is the same as the real name of another extension, the alternative name is disabled.
  - When 2 extensions have the same name among their alternative serialization ones, only one of them will be active. A warning is logged to tell which extension will be active for this name and which one is disabled.
- Previously, the XML namespace prefix could only have one value for an extension, determined by its SerDe class. Therefore, trying to import an XIIDM network containing an extension with a namespace prefix that didn't match the known prefix would fail. Now, the prefix could have any value supported by the XML format. (The prefix defined in the SerDe is now only used as a preferred one at export.)
- When exporting a network in XIIDM, if several extensions have the same namespace prefix in their SerDe (which could happen when using custom extensions), duplicates are renamed. For instance, if several extensions have the same prefix 'foo', the first one will be named 'foo' and the others 'foo1', 'foo2', ...


**Does this PR introduce a breaking change or deprecate an API?**
- [ ] Yes
- [X] No

**If yes, please check if the following requirements are fulfilled**
<!-- If no breaking changes or API deprecations were introduced, delete this section -->
- [ ] The *Breaking Change* or *Deprecated* label has been added
- [ ] The migration steps are described in the following section

**What changes might users need to make in their application due to this PR? (migration steps)**
<!-- If this PR introduces a breaking change, describe the migration steps -->
<!-- The content of this section will be copied in the migration guide -->



**Other information**:
<!-- if any of the questions/checkboxes don't apply, please delete them entirely -->



Diff:
diff --git a/commons/src/main/java/com/powsybl/commons/extensions/AbstractExtensionSerDe.java b/commons/src/main/java/com/powsybl/commons/extensions/AbstractExtensionSerDe.java
index c194853e242..fccf8c3f00c 100644
--- a/commons/src/main/java/com/powsybl/commons/extensions/AbstractExtensionSerDe.java
+++ b/commons/src/main/java/com/powsybl/commons/extensions/AbstractExtensionSerDe.java
@@ -9,6 +9,8 @@
 
 import java.io.InputStream;
 import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Stream;
 
 /**
  * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
@@ -42,6 +44,16 @@ public String getExtensionName() {
         return extensionName;
     }
 
+    @Override
+    public final String getSerializationName(String extensionVersion) {
+        return getExtensionName();
+    }
+
+    @Override
+    public final Set<String> getSerializationNames() {
+        return Set.of(getExtensionName());
+    }
+
     @Override
     public String getCategoryName() {
         return categoryName;
@@ -62,11 +74,21 @@ public String getNamespaceUri() {
         return namespaceUri;
     }
 
+    @Override
+    public Stream<String> getNamespaceUriStream() {
+        return Stream.of(namespaceUri);
+    }
+
     @Override
     public String getNamespacePrefix() {
         return namespacePrefix;
     }
 
+    @Override
+    public String getNamespacePrefix(String extensionVersion) {
+        return getNamespacePrefix();
+    }
+
     @Override
     public String getVersion(String namespaceUri) {
         return this.namespaceUri.equals(namespaceUri) ? getVersion() : null;
diff --git a/commons/src/main/java/com/powsybl/commons/extensions/DefaultExtensionProvidersLoader.java b/commons/src/main/java/com/powsybl/commons/extensions/DefaultExtensionProvidersLoader.java
new file mode 100644
index 00000000000..0ac68543822
--- /dev/null
+++ b/commons/src/main/java/com/powsybl/commons/extensions/DefaultExtensionProvidersLoader.java
@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.commons.extensions;
+
+import com.powsybl.commons.util.ServiceLoaderCache;
+
+import java.util.stream.Stream;
+
+/**
+ * @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>}
+ */
+public class DefaultExtensionProvidersLoader implements ExtensionProvidersLoader {
+
+    @Override
+    public <T extends ExtensionProvider> Stream<T> getServicesStream(Class<T> clazz) {
+        return new ServiceLoaderCache<>(clazz).getServices().stream();
+    }
+}
diff --git a/commons/src/main/java/com/powsybl/commons/extensions/ExtensionProviders.java b/commons/src/main/java/com/powsybl/commons/extensions/ExtensionProviders.java
index faeda78c612..eea0e22d02b 100644
--- a/commons/src/main/java/com/powsybl/commons/extensions/ExtensionProviders.java
+++ b/commons/src/main/java/com/powsybl/commons/extensions/ExtensionProviders.java
@@ -8,16 +8,20 @@
 package com.powsybl.commons.extensions;
 
 import com.powsybl.commons.PowsyblException;
-import com.powsybl.commons.util.ServiceLoaderCache;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.*;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * @author Mathieu Bague {@literal <mathieu.bague at rte-france.com>}
  */
 public final class ExtensionProviders<T extends ExtensionProvider> {
 
+    private static final Logger LOGGER = LoggerFactory.getLogger(ExtensionProviders.class);
+
     private final Map<String, T> providers;
 
     public static <T extends ExtensionProvider> ExtensionProviders<T> createProvider(Class<T> clazz) {
@@ -28,14 +32,17 @@ public static <T extends ExtensionProvider> ExtensionProviders<T> createProvider
         return new ExtensionProviders<>(clazz, categoryName);
     }
 
+    public static <T extends ExtensionProvider> ExtensionProviders<T> createProvider(Class<T> clazz, String categoryName, ExtensionProvidersLoader loader) {
+        return new ExtensionProviders<>(clazz, categoryName, null, loader);
+    }
+
     public static <T extends ExtensionProvider> ExtensionProviders<T> createProvider(Class<T> clazz, String categoryName, Set<String> extensionNames) {
         return new ExtensionProviders<>(clazz, categoryName, extensionNames);
     }
 
     private ExtensionProviders(Class<T> clazz) {
         Objects.requireNonNull(clazz);
-        providers = new ServiceLoaderCache<>(clazz).getServices().stream()
-                .collect(Collectors.toMap(T::getExtensionName, e -> e));
+        providers = loadProviders(clazz, null, null, new DefaultExtensionProvidersLoader());
     }
 
     private ExtensionProviders(Class<T> clazz, String categoryName) {
@@ -43,13 +50,57 @@ private ExtensionProviders(Class<T> clazz, String categoryName) {
     }
 
     private ExtensionProviders(Class<T> clazz, String categoryName, Set<String> extensionNames) {
+        this(clazz, categoryName, extensionNames, new DefaultExtensionProvidersLoader());
+    }
+
+    private ExtensionProviders(Class<T> clazz, String categoryName, Set<String> extensionNames, ExtensionProvidersLoader loader) {
         Objects.requireNonNull(clazz);
         Objects.requireNonNull(categoryName);
 
-        List<T> services = new ServiceLoaderCache<>(clazz).getServices();
-        providers = services.stream()
-                .filter(s -> s.getCategoryName().equals(categoryName) && (extensionNames == null || extensionNames.contains(s.getExtensionName())))
-                .collect(Collectors.toMap(T::getExtensionName, e -> e));
+        providers = loadProviders(clazz, categoryName, extensionNames, loader);
+    }
+
+    private Map<String, T> loadProviders(Class<T> clazz, String categoryName, Set<String> extensionNames, ExtensionProvidersLoader loader) {
+        final Map<String, T> providersMap = new HashMap<>();
+        Stream<T> servicesStream = loader.getServicesStream(clazz);
+        if (categoryName != null) {
+            servicesStream = servicesStream.filter(s -> s.getCategoryName().equals(categoryName));
+        }
+        Set<T> services = servicesStream.collect(Collectors.toSet());
+        services.forEach(service -> addService(providersMap, service, extensionNames));
+        if (clazz.equals(ExtensionSerDe.class)) {
+            // Add the alternative serialization names for extension SerDes
+            services.forEach(service -> ((ExtensionSerDe<?, ?>) service).getSerializationNames().stream()
+                    .filter(name -> !service.getExtensionName().equals(name))
+                    .forEach(name -> addServiceForAlternativeName(providersMap, service, name, extensionNames)));
+        }
+        return providersMap;
+    }
+
+    private void addService(Map<String, T> providersMap, T service, Set<String> extensionsToImport) {
+        String name = service.getExtensionName();
+        if (extensionsToImport == null || extensionsToImport.contains(name)) {
+            if (providersMap.containsKey(name)) {
+                // There should not be two extensions of the same real name
+                throw new IllegalStateException("Several providers were found for extension '" + name + "'");
+            } else {
+                providersMap.put(name, service);
+            }
+        }
+    }
+
+    private void addServiceForAlternativeName(Map<String, T> providersMap, T service, String alternativeName, Set<String> extensionsToImport) {
+        if (extensionsToImport == null || extensionsToImport.contains(service.getExtensionName()) || extensionsToImport.contains(alternativeName)) {
+            T previousService = providersMap.get(alternativeName);
+            if (previousService != null) {
+                // For alternative names, a duplicate does not replace the previous provider, to avoid replacing
+                // providers mapped to their real extension names by providers mapped to an alternative name.
+                LOGGER.warn("Alternative extension name {} for extension {} is already used for real extension {} - Skipping",
+                        alternativeName, service.getExtensionName(), previousService.getExtensionName());
+            } else {
+                providersMap.put(alternativeName, service);
+            }
+        }
     }
 
     public T findProvider(String name) {
@@ -66,10 +117,10 @@ public T findProviderOrThrowException(String name) {
     }
 
     public Collection<T> getProviders() {
-        return providers.values();
+        return providers.values().stream().distinct().collect(Collectors.toList());
     }
 
-    public <T> void addExtensions(Extendable<T> extendable, Collection<Extension<T>> extensions) {
+    public <E> void addExtensions(Extendable<E> extendable, Collection<Extension<E>> extensions) {
         Objects.requireNonNull(extendable);
         Objects.requireNonNull(extensions);
         extensions.forEach(e -> extendable.addExtension(findProvider(e.getName()).getExtensionClass(), e));
diff --git a/commons/src/main/java/com/powsybl/commons/extensions/ExtensionProvidersLoader.java b/commons/src/main/java/com/powsybl/commons/extensions/ExtensionProvidersLoader.java
new file mode 100644
index 00000000000..755580e6b13
--- /dev/null
+++ b/commons/src/main/java/com/powsybl/commons/extensions/ExtensionProvidersLoader.java
@@ -0,0 +1,18 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.commons.extensions;
+
+import java.util.stream.Stream;
+
+/**
+ * @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>}
+ */
+public interface ExtensionProvidersLoader {
+
+    <T extends ExtensionProvider> Stream<T> getServicesStream(Class<T> clazz);
+}
diff --git a/commons/src/main/java/com/powsybl/commons/extensions/ExtensionSerDe.java b/commons/src/main/java/com/powsybl/commons/extensions/ExtensionSerDe.java
index e53a9d0bee4..592fbba4e3c 100644
--- a/commons/src/main/java/com/powsybl/commons/extensions/ExtensionSerDe.java
+++ b/commons/src/main/java/com/powsybl/commons/extensions/ExtensionSerDe.java
@@ -14,6 +14,7 @@
 
 import java.io.InputStream;
 import java.util.*;
+import java.util.stream.Stream;
 
 /**
  * An ExtensionProvider able to serialize/deserialize extensions from XML.
@@ -44,6 +45,11 @@ default List<InputStream> getXsdAsStreamList() {
      */
     String getNamespaceUri();
 
+    /**
+     * Return the stream of all the namespace URIs of the extension, used for its XML (de)serialization
+     */
+    Stream<String> getNamespaceUriStream();
+
     /**
      * Return the namespace URI of the extension in a given version of its XML serialization.
      */
@@ -53,6 +59,8 @@ default String getNamespaceUri(String extensionVersion) {
 
     String getNamespacePrefix();
 
+    String getNamespacePrefix(String extensionVersion);
+
     void write(E extension, SerializerContext context);
 
     E read(T extendable, DeserializerContext context);
@@ -61,6 +69,10 @@ default String getName() {
         return getExtensionName();
     }
 
+    String getSerializationName(String extensionVersion);
+
+    Set<String> getSerializationNames();
+
     /**
      * Return the latest version of the serialization of the extension.
      */
diff --git a/commons/src/main/java/com/powsybl/commons/xml/XmlReader.java b/commons/src/main/java/com/powsybl/commons/xml/XmlReader.java
index 469aa5c006e..0ebc6217238 100644
--- a/commons/src/main/java/com/powsybl/commons/xml/XmlReader.java
+++ b/commons/src/main/java/com/powsybl/commons/xml/XmlReader.java
@@ -20,6 +20,7 @@
 import java.io.InputStream;
 import java.util.*;
 import java.util.function.Supplier;
+import java.util.stream.IntStream;
 
 /**
  * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
@@ -49,12 +50,12 @@ public String readRootVersion() {
 
     @Override
     public Map<String, String> readExtensionVersions() {
+        int namespaceCount = reader.getNamespaceCount();
+        List<String> namespaceUris = IntStream.range(0, namespaceCount).boxed().map(reader::getNamespaceURI).toList();
         Map<String, String> versions = new HashMap<>();
         for (ExtensionSerDe<?, ?> e : extensionProviders) {
-            String namespaceUri = reader.getNamespaceURI(e.getNamespacePrefix());
-            if (namespaceUri != null) {
-                versions.put(e.getExtensionName(), e.getVersion(namespaceUri));
-            }
+            e.getNamespaceUriStream().filter(namespaceUris::contains).findFirst()
+                    .ifPresent(uri -> versions.put(e.getExtensionName(), e.getVersion(uri)));
         }
         return versions;
     }
diff --git a/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/ExportOptions.java b/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/ExportOptions.java
index e9b0caaa6ff..5fdb35837d8 100644
--- a/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/ExportOptions.java
+++ b/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/ExportOptions.java
@@ -202,13 +202,17 @@ public ExportOptions setCharset(Charset charset) {
     }
 
     /**
-     * Add a given version in which the extension with the given name will be exported if
+     * <p>Add a given version in which the extension with the given name will be exported if
      * this version is supported by the extension's XML serializer and if it is compatible
-     * with the IIDM version in which the network will be exported.
-     * If the version is not added for an extension configured to be serialized, the extension will be serialized in the
-     * most recent version compatible with the IIDM version in which the network will be exported.
-     * If a version is added for an extension configured <b>not</b> to be serialized, the version will be ignored.
-     * If a version has already been added for the extension, throw an exception.
+     * with the IIDM version in which the network will be exported.</p>
+     * <ul>
+     * <li>If the version is not added for an extension configured to be serialized, the extension will be serialized in the
+     * most recent version compatible with the IIDM version in which the network will be exported.</li>
+     * <li>If a version is added for an extension configured <b>not</b> to be serialized, the version will be ignored.</li>
+     * <li>If a version has already been added for the extension, throw an exception.</li>
+     * </ul>
+     * <p>For extensions having multiple serialization names, only the real extension name is expected. If an alternative
+     * serialization name is used for the <code>extensionName</code> parameter, the method will have no effect.</p>
      */
     public ExportOptions addExtensionVersion(String extensionName, String extensionVersion) {
         if (extensions != null && !extensions.contains(extensionName)) {
diff --git a/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/NetworkSerDe.java b/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/NetworkSerDe.java
index de34f3a2a88..143ef856c71 100644
--- a/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/NetworkSerDe.java
+++ b/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/NetworkSerDe.java
@@ -18,7 +18,6 @@
 import com.powsybl.commons.exceptions.UncheckedSaxException;
 import com.powsybl.commons.exceptions.UncheckedXmlStreamException;
 import com.powsybl.commons.extensions.Extension;
-import com.powsybl.commons.extensions.ExtensionProviders;
 import com.powsybl.commons.extensions.ExtensionSerDe;
 import com.powsybl.commons.io.TreeDataFormat;
 import com.powsybl.commons.io.TreeDataHeader;
@@ -33,6 +32,8 @@
 import com.powsybl.iidm.serde.anonymizer.Anonymizer;
 import com.powsybl.iidm.serde.anonymizer.SimpleAnonymizer;
 import com.powsybl.iidm.serde.extensions.AbstractVersionableNetworkExtensionSerDe;
+import com.powsybl.iidm.serde.extensions.util.DefaultExtensionsSupplier;
+import com.powsybl.iidm.serde.extensions.util.ExtensionsSupplier;
 import com.powsybl.iidm.serde.util.IidmSerDeUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -70,7 +71,6 @@ public final class NetworkSerDe {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(NetworkSerDe.class);
 
-    private static final String EXTENSION_CATEGORY_NAME = "network";
     static final String NETWORK_ROOT_ELEMENT_NAME = "network";
     static final String NETWORK_ARRAY_ELEMENT_NAME = "networks";
     private static final String EXTENSION_ROOT_ELEMENT_NAME = "extension";
@@ -84,17 +84,25 @@ public final class NetworkSerDe {
     /** Magic number for binary iidm files ("Binary IIDM" in ASCII) */
     static final byte[] BIIDM_MAGIC_NUMBER = {0x42, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x20, 0x49, 0x49, 0x44, 0x4d};
 
-    private static final Supplier<ExtensionProviders<ExtensionSerDe>> EXTENSIONS_SUPPLIER =
-            Suppliers.memoize(() -> ExtensionProviders.createProvider(ExtensionSerDe.class, EXTENSION_CATEGORY_NAME));
-
-    private static final Supplier<Schema> SCHEMA_SUPPLIER = Suppliers.memoize(NetworkSerDe::createSchema);
+    private static final Supplier<Schema> DEFAULT_SCHEMA_SUPPLIER = Suppliers.memoize(() -> NetworkSerDe.createSchema(DefaultExtensionsSupplier.getInstance()));
+    private static final int MAX_NAMESPACE_PREFIX_NUM = 100;
 
     private NetworkSerDe() {
-        ExtensionProviders.createProvider(ExtensionSerDe.class, EXTENSION_CATEGORY_NAME);
     }
 
     public static void validate(InputStream is) {
-        Validator validator = SCHEMA_SUPPLIER.get().newValidator();
+        validate(is, DefaultExtensionsSupplier.getInstance());
+    }
+
+    public static void validate(InputStream is, ExtensionsSupplier extensionsSupplier) {
+        Objects.requireNonNull(extensionsSupplier);
+        Schema schema;
+        if (extensionsSupplier == DefaultExtensionsSupplier.getInstance()) {
+            schema = DEFAULT_SCHEMA_SUPPLIER.get();
+        } else {
+            schema = NetworkSerDe.createSchema(extensionsSupplier);
+        }
+        Validator validator = schema.newValidator();
         try {
             validator.validate(new StreamSource(is));
         } catch (IOException e) {
@@ -112,9 +120,9 @@ public static void validate(Path file) {
         }
     }
 
-    private static Schema createSchema() {
+    private static Schema createSchema(ExtensionsSupplier extensionsSupplier) {
         List<Source> additionalSchemas = new ArrayList<>();
-        for (ExtensionSerDe<?, ?> e : EXTENSIONS_SUPPLIER.get().getProviders()) {
+        for (ExtensionSerDe<?, ?> e : extensionsSupplier.get().getProviders()) {
             e.getXsdAsStreamList().forEach(xsd -> additionalSchemas.add(new StreamSource(xsd)));
         }
         SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
@@ -151,24 +159,25 @@ private static void throwExceptionIfOption(AbstractOptions<?> options, String me
         }
     }
 
-    private static void writeExtension(Extension<? extends Identifiable<?>> extension, NetworkSerializerContext context) {
+    private static void writeExtension(Extension<? extends Identifiable<?>> extension, NetworkSerializerContext context, ExtensionsSupplier extensionsSupplier) {
         TreeDataWriter writer = context.getWriter();
-        ExtensionSerDe extensionSerDe = getExtensionSerializer(context.getOptions(), extension);
+        ExtensionSerDe extensionSerDe = getExtensionSerializer(context.getOptions(), extension, extensionsSupplier);
         if (extensionSerDe == null) {
             throw new IllegalStateException("Extension Serializer of " + extension.getName() + " should not be null");
         }
         String namespaceUri = getNamespaceUri(extensionSerDe, context.getOptions());
-        writer.writeStartNode(namespaceUri, extension.getName());
         context.getExtensionVersion(extension.getName()).ifPresent(extensionSerDe::checkExtensionVersionSupported);
+        String exportName = extensionSerDe.getSerializationName(getExtensionVersion(extensionSerDe, context.getOptions()));
+        writer.writeStartNode(namespaceUri, exportName);
         extensionSerDe.write(extension, context);
         writer.writeEndNode();
     }
 
-    private static ExtensionSerDe getExtensionSerializer(ExportOptions options, Extension<? extends Identifiable<?>> extension) {
+    private static ExtensionSerDe getExtensionSerializer(ExportOptions options, Extension<? extends Identifiable<?>> extension, ExtensionsSupplier extensionsSupplier) {
         if (options.withExtension(extension.getName())) {
             ExtensionSerDe extensionSerDe = options.isThrowExceptionIfExtensionNotFound()
-                    ? EXTENSIONS_SUPPLIER.get().findProviderOrThrowException(extension.getName())
-                    : EXTENSIONS_SUPPLIER.get().findProvider(extension.getName());
+                    ? extensionsSupplier.get().findProviderOrThrowException(extension.getName())
+                    : extensionsSupplier.get().findProvider(extension.getName());
             if (extensionSerDe == null) {
                 String message = "XmlSerializer for " + extension.getName() + " not found";
                 throwExceptionIfOption(options, message);
@@ -196,21 +205,21 @@ private static void writeVoltageAngleLimits(Network n, NetworkSerializerContext
         }
     }
 
-    private static void writeExtensions(Network n, NetworkSerializerContext context) {
+    private static void writeExtensions(Network n, NetworkSerializerContext context, ExtensionsSupplier extensionsSupplier) {
         context.getWriter().writeStartNodes();
         for (Identifiable<?> identifiable : IidmSerDeUtil.sorted(n.getIdentifiables(), context.getOptions())) {
             if (!context.isExportedEquipment(identifiable) || !isElementWrittenInsideNetwork(identifiable, n, context)) {
                 continue;
             }
             Collection<? extends Extension<? extends Identifiable<?>>> extensions = identifiable.getExtensions().stream()
-                    .filter(e -> canTheExtensionBeWritten(getExtensionSerializer(context.getOptions(), e), context.getVersion(), context.getOptions()))
+                    .filter(e -> canTheExtensionBeWritten(getExtensionSerializer(context.getOptions(), e, extensionsSupplier), context.getVersion(), context.getOptions()))
                     .toList();
 
             if (!extensions.isEmpty()) {
                 context.getWriter().writeStartNode(context.getNamespaceURI(), EXTENSION_ROOT_ELEMENT_NAME);
                 context.getWriter().writeStringAttribute(ID, context.getAnonymizer().anonymizeString(identifiable.getId()));
                 for (Extension<? extends Identifiable<?>> extension : IidmSerDeUtil.sortedExtensions(extensions, context.getOptions())) {
-                    writeExtension(extension, context);
+                    writeExtension(extension, context, extensionsSupplier);
                 }
                 context.getWriter().writeEndNode();
             }
@@ -241,20 +250,42 @@ private static void writeMainAttributes(Network n, NetworkSerializerContext cont
         context.getWriter().writeStringAttribute(SOURCE_FORMAT, n.getSourceFormat());
     }
 
-    private static XmlWriter createXmlWriter(Network n, OutputStream os, ExportOptions options) {
+    private static XmlWriter createXmlWriter(Network n, OutputStream os, ExportOptions options, ExtensionsSupplier extensionsSupplier) {
         try {
             String iidmNamespace = options.getVersion().getNamespaceURI(n.getValidationLevel() == ValidationLevel.STEADY_STATE_HYPOTHESIS);
             String indent = options.isIndent() ? INDENT : null;
             XmlWriter xmlWriter = new XmlWriter(os, indent, options.getCharset(), iidmNamespace, IIDM_PREFIX);
 
-            Set<ExtensionSerDe<?, ?>> serializers = getExtensionSerializers(n, options);
+            Set<ExtensionSerDe<?, ?>> serializers = getExtensionSerializers(n, options, extensionsSupplier);
+            Set<String> extensionUris = new HashSet<>();
+            Set<String> extensionPrefixes = new HashSet<>();
             for (ExtensionSerDe<?, ?> extensionSerDe : serializers) {
                 String extensionVersion = getExtensionVersion(extensionSerDe, options);
-                xmlWriter.setExtensionNamespace(extensionSerDe.getName(), extensionSerDe.getNamespaceUri(extensionVersion), extensionSerDe.getNamespacePrefix());
-            }
+                String namespaceUri = extensionSerDe.getNamespaceUri(extensionVersion);
+                String realNamespacePrefix = extensionSerDe.getNamespacePrefix(extensionVersion);
+                String fixedNamespacePrefix = realNamespacePrefix;
 
-            // Ensure that there is no conflict in namespace prefixes and URIs
-            checkNamespaceCollisions(options, serializers);
+                // Throw an exception if a namespace URI collision is detected
+                if (extensionUris.contains(namespaceUri)) {
+                    throw new PowsyblException("Extension namespace URI collision");
+                } else {
+                    extensionUris.add(namespaceUri);
+                }
+
+                // Try to compute another namespace prefix if a collision is detected
+                int i = 1;
+                while (i < MAX_NAMESPACE_PREFIX_NUM && extensionPrefixes.contains(fixedNamespacePrefix)) {
+                    fixedNamespacePrefix = realNamespacePrefix + i;
+                    i++;
+                }
+                if (i >= MAX_NAMESPACE_PREFIX_NUM) {
+                    throw new PowsyblException("Cannot compute a unique extension namespace prefix: " + realNamespacePrefix);
+                } else {
+                    extensionPrefixes.add(fixedNamespacePrefix);
+                }
+
+                xmlWriter.setExtensionNamespace(extensionSerDe.getName(), namespaceUri, fixedNamespacePrefix);
+            }
 
             return xmlWriter;
         } catch (XMLStreamException e) {
@@ -262,9 +293,9 @@ private static XmlWriter createXmlWriter(Network n, OutputStream os, ExportOptio
         }
     }
 
-    private static JsonWriter createJsonWriter(OutputStream os, ExportOptions options) {
+    private static JsonWriter createJsonWriter(OutputStream os, ExportOptions options, ExtensionsSupplier extensionsSupplier) {
         try {
-            return new JsonWriter(os, options.isIndent(), options.getVersion().toString("."), createSingleNameToArrayNameMap(options));
+            return new JsonWriter(os, options.isIndent(), options.getVersion().toString("."), createSingleNameToArrayNameMap(options, extensionsSupplier));
         } catch (IOException e) {
             throw new UncheckedIOException(e);
         }
@@ -282,9 +313,9 @@ private static void writeRootElement(Network n, NetworkSerializerContext context
         writeMainAttributes(n, context);
     }
 
-    private static Map<String, String> getExtensionVersions(Network n, ExportOptions options) {
+    private static Map<String, String> getExtensionVersions(Network n, ExportOptions options, ExtensionsSupplier extensionsSupplier) {
         Map <String, String> extensionVersionsMap = new LinkedHashMap<>();
-        for (ExtensionSerDe<?, ?> extensionSerDe : getExtensionSerializers(n, options)) {
+        for (ExtensionSerDe<?, ?> extensionSerDe : getExtensionSerializers(n, options, extensionsSupplier)) {
             String version = getExtensionVersion(extensionSerDe, options);
             extensionVersionsMap.put(extensionSerDe.getExtensionName(), version);
         }
@@ -305,7 +336,7 @@ private static String getExtensionVersion(ExtensionSerDe<?, ?> extensionSerDe, E
     /**
      * Gets the list of the serializers needed to export the current network
      */
-    private static Set<ExtensionSerDe<?, ?>> getExtensionSerializers(Network n, ExportOptions options) {
+    private static Set<ExtensionSerDe<?, ?>> getExtensionSerializers(Network n, ExportOptions options, ExtensionsSupplier extensionsSupplier) {
         if (options.withNoExtension()) {
             return Collections.emptySet();
         }
@@ -313,37 +344,18 @@ private static String getExtensionVersion(ExtensionSerDe<?, ?> extensionSerDe, E
         IidmVersion networkVersion = options.getVersion();
         return n.getIdentifiables().stream().flatMap(identifiable -> identifiable.getExtensions()
                         .stream()
-                        .map(extension -> (ExtensionSerDe<?, ?>) getExtensionSerializer(options, extension))
+                        .map(extension -> (ExtensionSerDe<?, ?>) getExtensionSerializer(options, extension, extensionsSupplier))
                         .filter(exs -> canTheExtensionBeWritten(exs, networkVersion, options)))
                 .collect(Collectors.toCollection(LinkedHashSet::new));
     }
 
-    private static void checkNamespaceCollisions(ExportOptions options, Set<ExtensionSerDe<?, ?>> serializers) {
-        Set<String> extensionUris = new HashSet<>();
-        Set<String> extensionPrefixes = new HashSet<>();
-        for (ExtensionSerDe<?, ?> extensionSerDe : serializers) {
-            String namespaceUri = getNamespaceUri(extensionSerDe, options);
-            if (extensionUris.contains(namespaceUri)) {
-                throw new PowsyblException("Extension namespace URI collision");
-            } else {
-                extensionUris.add(namespaceUri);
-            }
-
-            if (extensionPrefixes.contains(extensionSerDe.getNamespacePrefix())) {
-                throw new PowsyblException("Extension namespace prefix collision");
-            } else {
-                extensionPrefixes.add(extensionSerDe.getNamespacePrefix());
-            }
-        }
-    }
-
-    private static void writeBaseNetwork(Network n, NetworkSerializerContext context) {
+    private static void writeBaseNetwork(Network n, NetworkSerializerContext context, ExtensionsSupplier extensionsSupplier) {
         IidmSerDeUtil.runFromMinimumVersion(IidmVersion.V_1_7, context, () -> context.getWriter().writeEnumAttribute(MINIMUM_VALIDATION_LEVEL, n.getValidationLevel()));
 
         AliasesSerDe.write(n, NETWORK_ROOT_ELEMENT_NAME, context);
         PropertiesSerDe.write(n, context);
 
-        IidmSerDeUtil.runFromMinimumVersion(IidmVersion.V_1_11, context, () -> writeSubnetworks(n, context));
+        IidmSerDeUtil.runFromMinimumVersion(IidmVersion.V_1_11, context, () -> writeSubnetworks(n, context, extensionsSupplier));
 
         writeVoltageLevels(n, context);
         writeSubstations(n, context);
@@ -354,12 +366,12 @@ private static void writeBaseNetwork(Network n, NetworkSerializerContext context
         IidmSerDeUtil.runFromMinimumVersion(IidmVersion.V_1_13, context, () -> writeAreas(n, context));
     }
 
-    private static void writeSubnetworks(Network n, NetworkSerializerContext context) {
+    private static void writeSubnetworks(Network n, NetworkSerializerContext context, ExtensionsSupplier extensionsSupplier) {
         context.getWriter().writeStartNodes();
         for (Network subnetwork : IidmSerDeUtil.sorted(n.getSubnetworks(), context.getOptions())) {
             IidmSerDeUtil.assertMinimumVersion(NETWORK_ROOT_ELEMENT_NAME, VoltageLevelSerDe.ROOT_ELEMENT_NAME,
                     IidmSerDeUtil.ErrorMessage.NOT_SUPPORTED, IidmVersion.V_1_11, context);
-            write(subnetwork, context);
+            write(subnetwork, context, extensionsSupplier);
         }
         context.getWriter().writeEndNodes();
     }
@@ -431,30 +443,34 @@ private static void writeHvdcLines(Network n, NetworkSerializerContext context)
         context.getWriter().writeEndNodes();
     }
 
-    private static TreeDataWriter createTreeDataWriter(Network n, ExportOptions options, OutputStream os) {
+    private static TreeDataWriter createTreeDataWriter(Network n, ExportOptions options, OutputStream os, ExtensionsSupplier extensionsSupplier) {
         return switch (options.getFormat()) {
-            case XML -> createXmlWriter(n, os, options);
-            case JSON -> createJsonWriter(os, options);
+            case XML -> createXmlWriter(n, os, options, extensionsSupplier);
+            case JSON -> createJsonWriter(os, options, extensionsSupplier);
             case BIN -> createBinWriter(os, options);
         };
     }
 
-    private static void write(Network network, NetworkSerializerContext context) {
+    private static void write(Network network, NetworkSerializerContext context, ExtensionsSupplier extensionsSupplier) {
         // consider the network has been exported so its extensions will be written
         // (should be done before extensions are written)
         context.addExportedEquipment(network);
         writeRootElement(network, context);
-        writeBaseNetwork(network, context);
+        writeBaseNetwork(network, context, extensionsSupplier);
         writeVoltageAngleLimits(network, context);
-        writeExtensions(network, context);
+        writeExtensions(network, context, extensionsSupplier);
         context.getWriter().writeEndNode();
     }
 
     public static Anonymizer write(Network n, ExportOptions options, OutputStream os) {
-        try (TreeDataWriter writer = createTreeDataWriter(n, options, os)) {
+        return write(n, options, os, DefaultExtensionsSupplier.getInstance());
+    }
+
+    public static Anonymizer write(Network n, ExportOptions options, OutputStream os, ExtensionsSupplier extensionsSupplier) {
+        try (TreeDataWriter writer = createTreeDataWriter(n, options, os, extensionsSupplier)) {
             NetworkSerializerContext context = createContext(n, options, writer);
-            writer.setVersions(getExtensionVersions(n, options));
-            write(n, context);
+            writer.setVersions(getExtensionVersions(n, options, extensionsSupplier));
+            write(n, context, extensionsSupplier);
             return context.getAnonymizer();
         }
     }
@@ -526,44 +542,48 @@ public static Network read(InputStream is, ImportOptions config, Anonymizer anon
     }
 
     public static Network read(InputStream is, ImportOptions config, Anonymizer anonymizer, NetworkFactory networkFactory, ReportNode reportNode) {
-        try (TreeDataReader reader = createTreeDataReader(is, config)) {
-            return read(reader, config, anonymizer, networkFactory, reportNode);
+        return read(is, config, anonymizer, networkFactory, DefaultExtensionsSupplier.getInstance(), reportNode);
+    }
+
+    public static Network read(InputStream is, ImportOptions config, Anonymizer anonymizer, NetworkFactory networkFactory, ExtensionsSupplier extensionsSupplier, ReportNode reportNode) {
+        try (TreeDataReader reader = createTreeDataReader(is, config, extensionsSupplier)) {
+            return read(reader, config, anonymizer, networkFactory, extensionsSupplier, reportNode);
         }
     }
 
-    private static TreeDataReader createTreeDataReader(InputStream is, ImportOptions config) {
+    private static TreeDataReader createTreeDataReader(InputStream is, ImportOptions config, ExtensionsSupplier extensionsSupplier) {
         return switch (config.getFormat()) {
-            case XML -> createXmlReader(is, config);
-            case JSON -> createJsonReader(is, config);
+            case XML -> createXmlReader(is, config, extensionsSupplier);
+            case JSON -> createJsonReader(is, config, extensionsSupplier);
             case BIN -> new BinReader(is, BIIDM_MAGIC_NUMBER);
         };
     }
 
-    private static TreeDataReader createJsonReader(InputStream is, ImportOptions config) {
+    private static TreeDataReader createJsonReader(InputStream is, ImportOptions config, ExtensionsSupplier extensionsSupplier) {
         try {
-            return new JsonReader(is, NETWORK_ROOT_ELEMENT_NAME, createArrayNameToSingleNameMap(config));
+            return new JsonReader(is, NETWORK_ROOT_ELEMENT_NAME, createArrayNameToSingleNameMap(config, extensionsSupplier));
         } catch (IOException e) {
             throw new UncheckedIOException(e);
         }
     }
 
-    private static TreeDataReader createXmlReader(InputStream is, ImportOptions config) {
+    private static TreeDataReader createXmlReader(InputStream is, ImportOptions config, ExtensionsSupplier extensionsSupplier) {
         try {
-            return new XmlReader(is, getNamespaceVersionMap(), config.withNoExtension() ? Collections.emptyList() : EXTENSIONS_SUPPLIER.get().getProviders());
+            return new XmlReader(is, getNamespaceVersionMap(), config.withNoExtension() ? Collections.emptyList() : extensionsSupplier.get().getProviders());
         } catch (XMLStreamException e) {
             throw new UncheckedXmlStreamException(e);
         }
     }
 
-    private static Map<String, String> createSingleNameToArrayNameMap(ExportOptions config) {
-        return createArrayNameSingleNameBiMap(!config.withNoExtension()).inverse();
+    private static Map<String, String> createSingleNameToArrayNameMap(ExportOptions config, ExtensionsSupplier extensionsSupplier) {
+        return createArrayNameSingleNameBiMap(!config.withNoExtension(), extensionsSupplier).inverse();
     }
 
-    private static Map<String, String> createArrayNameToSingleNameMap(ImportOptions config) {
-        return createArrayNameSingleNameBiMap(!config.withNoExtension());
+    private static Map<String, String> createArrayNameToSingleNameMap(ImportOptions config, ExtensionsSupplier extensionsSupplier) {
+        return createArrayNameSingleNameBiMap(!config.withNoExtension(), extensionsSupplier);
     }
 
-    private static BiMap<String, String> createArrayNameSingleNameBiMap(boolean withExtensions) {
+    private static BiMap<String, String> createArrayNameSingleNameBiMap(boolean withExtensions, ExtensionsSupplier extensionsSupplier) {
         Map<String, String> basicMap = Map.ofEntries(
                 Map.entry(NETWORK_ARRAY_ELEMENT_NAME, NETWORK_ROOT_ELEMENT_NAME),
                 Map.entry(EXTENSION_ARRAY_ELEMENT_NAME, EXTENSION_ROOT_ELEMENT_NAME),
@@ -606,7 +626,7 @@ private static BiMap<String, String> createArrayNameSingleNameBiMap(boolean with
 
         Map<String, String> extensionsMap = new HashMap<>();
         if (withExtensions) {
-            for (ExtensionSerDe<?, ?> e : EXTENSIONS_SUPPLIER.get().getProviders()) {
+            for (ExtensionSerDe<?, ?> e : extensionsSupplier.get().getProviders()) {
                 extensionsMap.putAll(e.getArrayNameToSingleNameMap());
             }
         }
@@ -628,11 +648,13 @@ private static Map<String, String> getNamespaceVersionMap() {
     }
 
     private static void readNetworkElement(String elementName, Deque<Network> networks, NetworkFactory networkFactory, NetworkDeserializerContext context,
-                                           Set<String> extensionNamesImported, Set<String> extensionNamesNotFound) {
+                                           Set<String> extensionNamesImported, Set<String> extensionNamesNotFound,
+                                           ExtensionsSupplier extensionsSupplier) {
         switch (elementName) {
             case AliasesSerDe.ROOT_ELEMENT_NAME -> checkSupportedAndReadAlias(networks.peek(), context);
             case PropertiesSerDe.ROOT_ELEMENT_NAME -> PropertiesSerDe.read(networks.peek(), context);
-            case NETWORK_ROOT_ELEMENT_NAME -> checkSupportedAndReadSubnetwork(networks, networkFactory, context, extensionNamesImported, extensionNamesNotFound);
+            case NETWORK_ROOT_ELEMENT_NAME -> checkSupportedAndReadSubnetwork(networks, networkFactory, context,
+                    extensionNamesImported, extensionNamesNotFound, extensionsSupplier);
             case AreaSerDe.ROOT_ELEMENT_NAME -> checkSupportedAndReadArea(context, networks);
             case VoltageLevelSerDe.ROOT_ELEMENT_NAME -> checkSupportedAndReadVoltageLevel(context, networks);
             case SubstationSerDe.ROOT_ELEMENT_NAME -> SubstationSerDe.INSTANCE.read(networks.peek(), context);
@@ -640,7 +662,8 @@ private static void readNetworkElement(String elementName, Deque<Network> networ
             case TieLineSerDe.ROOT_ELEMENT_NAME -> TieLineSerDe.INSTANCE.read(networks.peek(), context);
             case HvdcLineSerDe.ROOT_ELEMENT_NAME -> HvdcLineSerDe.INSTANCE.read(networks.peek(), context);
             case VoltageAngleLimitSerDe.ROOT_ELEMENT_NAME -> VoltageAngleLimitSerDe.read(networks.peek(), context);
-            case EXTENSION_ROOT_ELEMENT_NAME -> findExtendableAndReadExtension(networks.getFirst(), context, extensionNamesImported, extensionNamesNotFound);
+            case EXTENSION_ROOT_ELEMENT_NAME -> findExtendableAndReadExtension(networks.getFirst(), context,
+                    extensionNamesImported, extensionNamesNotFound, extensionsSupplier);
             default -> throw new PowsyblException("Unknown element name '" + elementName + "' in 'network'");
         }
     }
@@ -651,7 +674,8 @@ private static void checkSupportedAndReadAlias(Network network, NetworkDeseriali
     }
 
     private static void checkSupportedAndReadSubnetwork(Deque<Network> networks, NetworkFactory networkFactory, NetworkDeserializerContext context,
-                                                        Set<String> extensionNamesImported, Set<String> extensionNamesNotFound) {
+                                                        Set<String> extensionNamesImported, Set<String> extensionNamesNotFound,
+                                                        ExtensionsSupplier extensionsSupplier) {
         IidmSerDeUtil.assertMinimumVersion(NETWORK_ROOT_ELEMENT_NAME, NETWORK_ROOT_ELEMENT_NAME,
                 IidmSerDeUtil.ErrorMessage.NOT_SUPPORTED, IidmVersion.V_1_11, context);
         if (networks.size() > 1) {
@@ -662,7 +686,8 @@ private static void checkSupportedAndReadSubnetwork(Deque<Network> networks, Net
         networks.push(subnetwork);
         // Read subnetwork content
         context.getReader().readChildNodes(
-                elementName -> readNetworkElement(elementName, networks, networkFactory, context, extensionNamesImported, extensionNamesNotFound));
+                elementName -> readNetworkElement(elementName, networks, networkFactory, context,
+                        extensionNamesImported, extensionNamesNotFound, extensionsSupplier));
         // Pop the subnetwork. We will now work with its parent.
         networks.pop();
     }
@@ -679,13 +704,15 @@ private static void checkSupportedAndReadVoltageLevel(NetworkDeserializerContext
         VoltageLevelSerDe.INSTANCE.read(networks.peek(), context);
     }
 
-    private static void findExtendableAndReadExtension(Network network, NetworkDeserializerContext context, Set<String> extensionNamesImported, Set<String> extensionNamesNotFound) {
+    private static void findExtendableAndReadExtension(Network network, NetworkDeserializerContext context,
+                                                       Set<String> extensionNamesImported, Set<String> extensionNamesNotFound,
+                                                       ExtensionsSupplier extensionsSupplier) {
         String id2 = context.getAnonymizer().deanonymizeString(context.getReader().readStringAttribute("id"));
         Identifiable identifiable = network.getIdentifiable(id2);
         if (identifiable == null) {
             throw new PowsyblException("Identifiable " + id2 + " not found");
         }
-        readExtensions(identifiable, context, extensionNamesImported, extensionNamesNotFound);
+        readExtensions(identifiable, context, extensionNamesImported, extensionNamesNotFound, extensionsSupplier);
     }
 
     private static Network initNetwork(NetworkFactory networkFactory, NetworkDeserializerContext context, TreeDataReader reader, Network rootNetwork) {
@@ -732,6 +759,11 @@ private static void logExtensionsNotFound(ReportNode reportNode, Set<String> ext
 
     public static Network read(TreeDataReader reader, ImportOptions config, Anonymizer anonymizer,
                                NetworkFactory networkFactory, ReportNode reportNode) {
+        return read(reader, config, anonymizer, networkFactory, DefaultExtensionsSupplier.getInstance(), reportNode);
+    }
+
+    public static Network read(TreeDataReader reader, ImportOptions config, Anonymizer anonymizer,
+                               NetworkFactory networkFactory, ExtensionsSupplier extensionsSupplier, ReportNode reportNode) {
         Objects.requireNonNull(reader);
         Objects.requireNonNull(networkFactory);
         Objects.requireNonNull(reportNode);
@@ -750,7 +782,7 @@ public static Network read(TreeDataReader reader, ImportOptions config, Anonymiz
 
         ReportNode validationReportNode = reportNode.newReportNode().withMessageTemplate("validationWarnings", "Validation warnings").add();
         reader.readChildNodes(elementName ->
-                readNetworkElement(elementName, networks, networkFactory, context, extensionNamesImported, extensionNamesNotFound));
+                readNetworkElement(elementName, networks, networkFactory, context, extensionNamesImported, extensionNamesNotFound, extensionsSupplier));
 
         if (!extensionNamesImported.isEmpty()) {
             ReportNode importedExtensionReportNode = reportNode.newReportNode().withMessageTemplate("importedExtensions", "Imported extensions").add();
@@ -758,7 +790,7 @@ public static Network read(TreeDataReader reader, ImportOptions config, Anonymiz
         }
         if (!extensionNamesNotFound.isEmpty()) {
             ReportNode extensionsNotFoundReportNode = reportNode.newReportNode().withMessageTemplate("extensionsNotFound", "Not found extensions").add();
-            throwExceptionIfOption(context.getOptions(), "Extensions " + extensionNamesNotFound + " " + "not found !");
+            throwExceptionIfOption(context.getOptions(), "Extensions " + extensionNamesNotFound + " " + "not found!");
             logExtensionsNotFound(extensionsNotFoundReportNode, extensionNamesNotFound);
         }
 
@@ -816,16 +848,18 @@ public static Network validateAndRead(Path xmlFile) {
     }
 
     private static void readExtensions(Identifiable identifiable, NetworkDeserializerContext context,
-                                       Set<String> extensionNamesImported, Set<String> extensionNamesNotFound) {
+                                       Set<String> extensionNamesImported, Set<String> extensionNamesNotFound,
+                                       ExtensionsSupplier extensionsSupplier) {
 
-        context.getReader().readChildNodes(extensionName -> {
+        context.getReader().readChildNodes(extensionSerializationName -> {
             // extensions root elements are nested directly in 'extension' element, so there is no need
             // to check for an extension to exist if depth is greater than zero. Furthermore, in case of
             // missing extension serializer, we must not check for an extension in sub elements.
-            if (context.getOptions().withExtension(extensionName)) {
-                ExtensionSerDe extensionXmlSerializer = EXTENSIONS_SUPPLIER.get().findProvider(extensionName);
-                if (extensionXmlSerializer != null) {
-                    extensionXmlSerializer.read(identifiable, context);
+            ExtensionSerDe extensionSerde = extensionsSupplier.get().findProvider(extensionSerializationName);
+            String extensionName = extensionSerde != null ? extensionSerde.getExtensionName() : extensionSerializationName;
+            if (context.getOptions().withExtension(extensionName) || context.getOptions().withExtension(extensionSerializationName)) {
+                if (extensionSerde != null) {
+                    extensionSerde.read(identifiable, context);
                     extensionNamesImported.add(extensionName);
                 } else {
                     extensionNamesNotFound.add(extensionName);
diff --git a/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/extensions/AbstractVersionableNetworkExtensionSerDe.java b/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/extensions/AbstractVersionableNetworkExtensionSerDe.java
index 6c135408c25..7233813dae6 100644
--- a/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/extensions/AbstractVersionableNetworkExtensionSerDe.java
+++ b/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/extensions/AbstractVersionableNetworkExtensionSerDe.java
@@ -19,6 +19,7 @@
 import com.powsybl.iidm.serde.NetworkDeserializerContext;
 
 import java.util.*;
+import java.util.stream.Stream;
 
 /**
  * @author Miora Ralambotiana {@literal <miora.ralambotiana at rte-france.com>}
@@ -32,14 +33,44 @@ public abstract class AbstractVersionableNetworkExtensionSerDe<T extends Extenda
     private final String namespacePrefix;
     private final Map<IidmVersion, ImmutableSortedSet<String>> extensionVersions = new EnumMap<>(IidmVersion.class);
     private final BiMap<String, String> namespaceUris = HashBiMap.create();
+    private final Map<String, String> serializationNameByVersion = new HashMap<>();
+    private final Map<String, String> namespacePrefixByVersion = new HashMap<>();
+
+    public record AlternativeSerializationData(String name, List<String> versions, String namespacePrefix) {
+        public AlternativeSerializationData(String name, List<String> versions) {
+            this(name, versions, null);
+        }
+    }
 
     protected AbstractVersionableNetworkExtensionSerDe(String extensionName, Class<? super E> extensionClass, String namespacePrefix,
                                                        Map<IidmVersion, ImmutableSortedSet<String>> extensionVersions, Map<String, String> namespaceUris) {
+        this(extensionName, extensionClass, namespacePrefix, extensionVersions, namespaceUris, null);
+    }
+
+    protected AbstractVersionableNetworkExtensionSerDe(String extensionName, Class<? super E> extensionClass, String namespacePrefix,
+                                                       Map<IidmVersion, ImmutableSortedSet<String>> extensionVersions,
+                                                       Map<String, String> namespaceUris,
+                                                       List<AlternativeSerializationData> alternativeSerializationData) {
         this.extensionName = Objects.requireNonNull(extensionName);
         this.extensionClass = Objects.requireNonNull(extensionClass);
         this.namespacePrefix = Objects.requireNonNull(namespacePrefix);
         this.extensionVersions.putAll(Objects.requireNonNull(extensionVersions));
         this.namespaceUris.putAll(Objects.requireNonNull(namespaceUris));
+
+        if (alternativeSerializationData != null) {
+            Set<String> names = new HashSet<>();
+            for (AlternativeSerializationData data : alternativeSerializationData) {
+                if (!names.add(data.name())) {
+                    throw new IllegalArgumentException("Duplicate alternative serialization name: " + data.name());
+                }
+                data.versions().forEach(version -> {
+                    this.serializationNameByVersion.put(version, data.name());
+                    if (data.namespacePrefix() != null) {
+                        this.namespacePrefixByVersion.put(version, data.namespacePrefix());
+                    }
+                });
+            }
+        }
     }
 
     @Override
@@ -47,6 +78,18 @@ public String getExtensionName() {
         return extensionName;
     }
 
+    @Override
+    public String getSerializationName(String extensionVersion) {
+        return serializationNameByVersion.getOrDefault(extensionVersion, extensionName);
+    }
+
+    @Override
+    public Set<String> getSerializationNames() {
+        Set<String> names = new HashSet<>(serializationNameByVersion.values());
+        names.add(extensionName);
+        return names;
+    }
+
     @Override
     public String getCategoryName() {
         return "network";
@@ -62,6 +105,11 @@ public String getNamespaceUri() {
         return getNamespaceUri(getVersion());
     }
 
+    @Override
+    public Stream<String> getNamespaceUriStream() {
+        return namespaceUris.values().stream().distinct();
+    }
+
     @Override
     public String getNamespaceUri(String extensionVersion) {
         return Optional.ofNullable(namespaceUris.get(extensionVersion))
@@ -127,6 +175,11 @@ public String getNamespacePrefix() {
         return namespacePrefix;
     }
 
+    @Override
+    public String getNamespacePrefix(String extensionVersion) {
+        return namespacePrefixByVersion.getOrDefault(extensionVersion, namespacePrefix);
+    }
+
     @Override
     public void checkExtensionVersionSupported(String extensionVersion) {
         if (!namespaceUris.containsKey(extensionVersion)) {
diff --git a/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/extensions/util/DefaultExtensionsSupplier.java b/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/extensions/util/DefaultExtensionsSupplier.java
new file mode 100644
index 00000000000..627a27ef6e1
--- /dev/null
+++ b/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/extensions/util/DefaultExtensionsSupplier.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.iidm.serde.extensions.util;
+
+import com.google.common.base.Suppliers;
+import com.powsybl.commons.extensions.ExtensionProviders;
+import com.powsybl.commons.extensions.ExtensionSerDe;
+
+import java.util.function.Supplier;
+
+/**
+ * @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>}
+ */
+public final class DefaultExtensionsSupplier implements ExtensionsSupplier {
+    private static final String EXTENSION_CATEGORY_NAME = "network";
+
+    private static final Supplier<ExtensionProviders<ExtensionSerDe>> EXTENSIONS_SUPPLIER =
+            Suppliers.memoize(() -> ExtensionProviders.createProvider(ExtensionSerDe.class, EXTENSION_CATEGORY_NAME));
+
+    private DefaultExtensionsSupplier() {
+    }
+
+    // Bill Pugh Singleton Implementation
+    private static class SingletonHelper {
+        private static final DefaultExtensionsSupplier INSTANCE = new DefaultExtensionsSupplier();
+    }
+
+    public static DefaultExtensionsSupplier getInstance() {
+        return SingletonHelper.INSTANCE;
+    }
+
+    @Override
+    public ExtensionProviders<ExtensionSerDe> get() {
+        return EXTENSIONS_SUPPLIER.get();
+    }
+}
diff --git a/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/extensions/util/ExtensionsSupplier.java b/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/extensions/util/ExtensionsSupplier.java
new file mode 100644
index 00000000000..ab40f69142c
--- /dev/null
+++ b/iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/extensions/util/ExtensionsSupplier.java
@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.iidm.serde.extensions.util;
+
+import com.powsybl.commons.extensions.ExtensionProviders;
+import com.powsybl.commons.extensions.ExtensionSerDe;
+
+/**
+ * @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>}
+ */
+@FunctionalInterface
+public interface ExtensionsSupplier {
+    ExtensionProviders<ExtensionSerDe> get();
+}
diff --git a/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/LoadFooSerDe.java b/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/LoadFooSerDe.java
index 6671c274bd1..8cb60048575 100644
--- a/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/LoadFooSerDe.java
+++ b/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/LoadFooSerDe.java
@@ -28,6 +28,7 @@ public LoadFooSerDe() {
 
     @Override
     public void write(LoadFooExt loadFoo, SerializerContext context) {
+        // empty extension
     }
 
     @Override
diff --git a/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/LoadMockSerDe.java b/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/LoadMockSerDe.java
index 623bfc2feee..0f4584bb203 100644
--- a/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/LoadMockSerDe.java
+++ b/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/LoadMockSerDe.java
@@ -8,7 +8,6 @@
 package com.powsybl.iidm.serde;
 
 import com.google.auto.service.AutoService;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSortedSet;
 import com.powsybl.commons.extensions.ExtensionSerDe;
@@ -20,6 +19,7 @@
 
 import java.io.InputStream;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * @author Miora Ralambotiana {@literal <miora.ralambotiana at rte-france.com>}
@@ -30,14 +30,19 @@ public class LoadMockSerDe extends AbstractVersionableNetworkExtensionSerDe<Load
     public LoadMockSerDe() {
         super("loadMock", LoadMockExt.class, "lmock",
                 ImmutableMap.<IidmVersion, ImmutableSortedSet<String>>builder()
-                        .put(IidmVersion.V_1_0, ImmutableSortedSet.of("1.0"))
+                        .put(IidmVersion.V_1_0, ImmutableSortedSet.of("0.1", "0.2", "1.0"))
                         .put(IidmVersion.V_1_1, ImmutableSortedSet.of("1.1", "1.2"))
+                        .put(IidmSerDeConstants.CURRENT_IIDM_VERSION, ImmutableSortedSet.of("1.1", "1.2"))
                         .build(),
                 ImmutableMap.<String, String>builder()
+                        .put("0.1", "http://www.powsybl.org/schema/iidm/ext/load_element_mock/1_0")
+                        .put("0.2", "http://www.powsybl.org/schema/iidm/ext/load_element_mock/1_1")
                         .put("1.0", "http://www.powsybl.org/schema/iidm/ext/load_mock/1_0")
                         .put("1.1", "http://www.powsybl.org/schema/iidm/ext/load_mock/1_1")
                         .put("1.2", "http://www.powsybl.org/schema/iidm/ext/load_mock/1_2")
-                        .build());
+                        .build(),
+                List.of(new AlternativeSerializationData("loadElementMock", List.of("0.1")),
+                        new AlternativeSerializationData("loadEltMock", List.of("0.2"), "lem")));
     }
 
     @Override
@@ -47,14 +52,16 @@ public InputStream getXsdAsStream() {
 
     @Override
     public List<InputStream> getXsdAsStreamList() {
-        return ImmutableList.of(getClass().getResourceAsStream("/V1_0/xsd/loadMock_V1_0.xsd"),
-                getClass().getResourceAsStream("/V1_1/xsd/loadMock_V1_1.xsd"),
-                getClass().getResourceAsStream("/V1_1/xsd/loadMock_V1_2.xsd"));
+        return List.of(Objects.requireNonNull(getClass().getResourceAsStream("/V1_0/xsd/loadMock_V0_1.xsd")),
+                Objects.requireNonNull(getClass().getResourceAsStream("/V1_0/xsd/loadMock_V0_2.xsd")),
+                Objects.requireNonNull(getClass().getResourceAsStream("/V1_0/xsd/loadMock_V1_0.xsd")),
+                Objects.requireNonNull(getClass().getResourceAsStream("/V1_1/xsd/loadMock_V1_1.xsd")),
+                Objects.requireNonNull(getClass().getResourceAsStream("/V1_1/xsd/loadMock_V1_2.xsd")));
     }
 
     @Override
     public void write(LoadMockExt extension, SerializerContext context) {
-        // do nothing
+        // empty extension
     }
 
     @Override
diff --git a/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/VoltageLevelFooSerDe.java b/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/VoltageLevelFooSerDe.java
new file mode 100644
index 00000000000..505798c717b
--- /dev/null
+++ b/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/VoltageLevelFooSerDe.java
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.iidm.serde;
+
+import com.google.auto.service.AutoService;
+import com.powsybl.commons.extensions.AbstractExtensionSerDe;
+import com.powsybl.commons.extensions.ExtensionSerDe;
+import com.powsybl.commons.io.DeserializerContext;
+import com.powsybl.commons.io.SerializerContext;
+import com.powsybl.iidm.network.VoltageLevel;
+import com.powsybl.iidm.network.test.VoltageLevelFooExt;
+
+/**
+ * @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>}
+ */
+@AutoService(ExtensionSerDe.class)
+public class VoltageLevelFooSerDe extends AbstractExtensionSerDe<VoltageLevel, VoltageLevelFooExt> {
+
+    public VoltageLevelFooSerDe() {
+        super("voltageLevelFoo", "network", VoltageLevelFooExt.class, "voltageLevelFoo.xsd",
+                "http://www.itesla_project.eu/schema/iidm/ext/voltagelevelfoo/1_0", "foo");
+    }
+
+    @Override
+    public void write(VoltageLevelFooExt voltageLevelFoo, SerializerContext context) {
+        // empty extension
+    }
+
+    @Override
+    public VoltageLevelFooExt read(VoltageLevel voltageLevel, DeserializerContext context) {
+        context.getReader().readEndNode();
+        var ext = new VoltageLevelFooExt(voltageLevel);
+        voltageLevel.addExtension(VoltageLevelFooExt.class, ext);
+        return ext;
+    }
+}
diff --git a/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/XMLExporterTest.java b/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/XMLExporterTest.java
index ade07d64a3a..f8586ad55f7 100644
--- a/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/XMLExporterTest.java
+++ b/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/XMLExporterTest.java
@@ -11,13 +11,9 @@
 import com.google.common.jimfs.Jimfs;
 import com.powsybl.commons.PowsyblException;
 import com.powsybl.commons.datasource.MemDataSource;
-import com.powsybl.iidm.network.Bus;
-import com.powsybl.iidm.network.Network;
-import com.powsybl.iidm.network.Terminal;
-import com.powsybl.iidm.network.VoltageLevel;
+import com.powsybl.iidm.network.*;
 import com.powsybl.iidm.network.extensions.SlackTerminalAdder;
-import com.powsybl.iidm.network.test.EurostagTutorialExample1Factory;
-import com.powsybl.iidm.network.test.MultipleExtensionsTestNetworkFactory;
+import com.powsybl.iidm.network.test.*;
 import org.junit.jupiter.api.Test;
 
 import java.io.ByteArrayInputStream;
@@ -27,6 +23,7 @@
 import java.nio.file.Path;
 import java.time.ZonedDateTime;
 import java.util.Properties;
+import java.util.function.Supplier;
 
 import static com.powsybl.commons.test.ComparisonUtils.assertXmlEquals;
 import static com.powsybl.iidm.serde.IidmSerDeConstants.CURRENT_IIDM_VERSION;
@@ -40,12 +37,16 @@ class XMLExporterTest extends AbstractIidmSerDeTest {
     private FileSystem fileSystem;
 
     void exporterTest(Network network, IidmVersion version, String xmlFileName, Properties properties) throws IOException {
+        exporterTest(network, () -> getVersionedNetworkAsStream(xmlFileName, version), properties);
+    }
+
+    void exporterTest(Network network, Supplier<InputStream> refFileIs, Properties properties) throws IOException {
         properties.put(XMLExporter.ANONYMISED, "false");
         MemDataSource dataSource = new MemDataSource();
         new XMLExporter().export(network, properties, dataSource);
         // check the exported file and compare it to iidm reference file
         try (InputStream is = new ByteArrayInputStream(dataSource.getData(null, "xiidm"))) {
-            assertXmlEquals(getVersionedNetworkAsStream(xmlFileName, version), is);
+            assertXmlEquals(refFileIs.get(), is);
         }
     }
 
@@ -54,6 +55,28 @@ void exportTest() throws IOException {
         exporterTest(MultipleExtensionsTestNetworkFactory.create(), CURRENT_IIDM_VERSION, "multiple-extensions.xml", new Properties());
     }
 
+    @Test
+    void exportWithNamespacePrefixCollisionTest() throws IOException {
+        Network network = MultipleExtensionsTestNetworkFactory.create();
+        VoltageLevel vl = network.getVoltageLevel("VL");
+        vl.addExtension(VoltageLevelFooExt.class, new VoltageLevelFooExt(vl));
+        exporterTest(network, () -> getClass().getResourceAsStream("/namespace-prefix-collision.xml"), new Properties());
+    }
+
+    @Test
+    void exportXiidmWithAnotherPrefixTest() throws IOException {
+        Network network = NetworkFactory.findDefault().createNetwork("test", "test");
+        network.setCaseDate(ZonedDateTime.parse("2024-09-17T13:36:37.831Z"));
+        Substation s1 = network.newSubstation().setId("S1").add();
+        VoltageLevel vl1 = s1.newVoltageLevel().setId("VL1").setNominalV(450).setTopologyKind(TopologyKind.NODE_BREAKER).add();
+        Load load1 = vl1.newLoad().setId("Load1").setNode(0).setP0(10.).setQ0(20.).add();
+        load1.addExtension(LoadMockExt.class, new LoadMockExt(load1));
+        Properties properties = new Properties();
+        properties.put(XMLExporter.VERSION, IidmVersion.V_1_0.toString("."));
+        properties.put("iidm.export.xml.loadMock.version", "0.2");
+        exporterTest(network, () -> getClass().getResourceAsStream("/extensionName_0_2.xml"), properties);
+    }
+
     @Test
     void paramsTest() {
         var xmlExporter = new XMLExporter();
diff --git a/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/extensions/SerializationNamesTest.java b/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/extensions/SerializationNamesTest.java
new file mode 100644
index 00000000000..496dd251bf1
--- /dev/null
+++ b/iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/extensions/SerializationNamesTest.java
@@ -0,0 +1,178 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.iidm.serde.extensions;
+
+import com.powsybl.commons.extensions.*;
+import com.powsybl.commons.io.DeserializerContext;
+import com.powsybl.commons.io.SerializerContext;
+import com.powsybl.commons.report.ReportNode;
+import com.powsybl.commons.util.ServiceLoaderCache;
+import com.powsybl.iidm.network.*;
+import com.powsybl.iidm.network.test.LoadFooExt;
+import com.powsybl.iidm.network.test.LoadMockExt;
+import com.powsybl.iidm.serde.*;
+import com.powsybl.iidm.serde.extensions.util.ExtensionsSupplier;
+import com.powsybl.iidm.serde.extensions.util.NetworkSourceExtensionSerDe;
+import org.junit.jupiter.api.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>}
+ */
+class SerializationNamesTest extends AbstractIidmSerDeTest {
+
+    public static final String LOAD_MOCK = "loadMock";
+    public static final String LOAD_ELEMENT_MOCK = "loadElementMock";
+
+    @Test
+    void unversionedExtensionTest() {
+        // Use the default extension version => also the default serialization name
+        AbstractExtensionSerDe<?, ?> serde = new NetworkSourceExtensionSerDe();
+        assertEquals(serde.getExtensionName(), serde.getSerializationName(serde.getVersion()));
+        Set<String> serializationNames = serde.getSerializationNames();
+        assertEquals(1, serializationNames.size());
+        assertTrue(serializationNames.contains("networkSource"));
+    }
+
+    @Test
+    void getSerializationNameTest() {
+        // Use an extension version which serialization name is not the default
+        LoadMockSerDe serde = new LoadMockSerDe();
+        assertEquals(LOAD_MOCK, serde.getExtensionName());
+        assertEquals(LOAD_MOCK, serde.getSerializationName(serde.getVersion()));
+        assertEquals(LOAD_ELEMENT_MOCK, serde.getSerializationName("0.1"));
+        Set<String> serializationNames = serde.getSerializationNames();
+        assertEquals(3, serializationNames.size());
+        assertTrue(serializationNames.containsAll(Set.of(LOAD_MOCK, LOAD_ELEMENT_MOCK, "loadEltMock")));
+    }
+
+    @Test
+    void realExtensionNameRoundTripTest() throws IOException {
+        // Use the default extension version => also the default serialization name
+        allFormatsRoundTripTest(getNetwork(), "/extensionName_1_2.xml", new ExportOptions().setVersion(IidmVersion.V_1_1.toString(".")));
+    }
+
+    @Test
+    void oldNameRoundTripTest() throws IOException {
+        // Use an extension version which serialization name is not the default
+        ExportOptions exportOptions = new ExportOptions()
+                .setVersion(IidmVersion.V_1_0.toString("."))
+                .addExtensionVersion("loadMock", "0.1");
+        allFormatsRoundTripTest(getNetwork(), "/extensionName_0_1.xml", exportOptions);
+    }
+
+    @Test
+    void importXiidmWithAnotherPrefixTest() throws IOException {
+        // Read a network with an old extension serialization name + a non-standard namespace prefix.
+        String file = "/extensionName_0_1_otherPrefix.xml";
+        try (InputStream is = getClass().getResourceAsStream(file)) {
+            NetworkSerDe.validate(is);
+        }
+        Network network = NetworkSerDe.read(getClass().getResourceAsStream(file));
+        assertNotNull(network.getLoad("Load1").getExtension(LoadMockExt.class));
+    }
+
+    @Test
+    void indicateExtensionAtImportTest() {
+        // Read a network with an old extension serialization name
+        // To specify the extension to import, both the real extension name or the serialization name can be used.
+        String file = "/extensionName_0_1_otherPrefix.xml";
+        ImportOptions importOptions = new ImportOptions().addExtension("loadElementMock");
+        Network network = NetworkSerDe.read(getClass().getResourceAsStream(file), importOptions, null);
+        assertNotNull(network.getLoad("Load1").getExtension(LoadMockExt.class),
+                "Using the serialization name as extension to load should work.");
+
+        importOptions = new ImportOptions().addExtension("loadMock");
+        network = NetworkSerDe.read(getClass().getResourceAsStream(file), importOptions, null);
+        assertNotNull(network.getLoad("Load1").getExtension(LoadMockExt.class),
+                "Using the real extension name as extension to load should work.");
+    }
+
+    @Test
+    void ignoreOtherSerializationNameIfAlreadyUsed() {
+        // No extension serde available
+        // This assertion is used to test that the configuration mechanism is working
+        Network network = loadNetworkWithGivenSerdes(List.of());
+        assertTrue(network.getLoad("Load1").getExtensions().isEmpty(), "No extension should be loaded.");
+
+        // The extension in the network ("loadElementMock") is only associated to the LoadMockExt extension,
+        // and it is defined as an "other serialization name"
+        List<ExtensionSerDe<?, ?>> serdes = List.of(new LoadMockSerDe());
+        network = loadNetworkWithGivenSerdes(serdes);
+        Load load1 = network.getLoad("Load1");
+        assertNotNull(load1.getExtension(LoadMockExt.class), "The read extension should be a LoadMockExt.");
+        assertEquals(1, load1.getExtensions().size(), "Only 1 extension should be loaded.");
+
+        // The extension in the network corresponds to the real extension name of the dummy extension SerDe
+        // and to an "other serialization name" of the LoadMockExt extension.
+        // In this case, the dummy extension should have the precedence (real names before "other" names)
+        // Note that the dummy SerDe loads a "LoadFooExt".
+        serdes = List.of(new LoadMockSerDe(), createDummyExtensionSerDe());
+        network = loadNetworkWithGivenSerdes(serdes);
+        load1 = network.getLoad("Load1");
+        assertNull(load1.getExtension(LoadMockExt.class), "No LoadMockExt should be loaded.");
+        assertNotNull(load1.getExtension(LoadFooExt.class), "A LoadFooExt extension should be loaded.");
+    }
+
+    private Network loadNetworkWithGivenSerdes(List<ExtensionSerDe<?, ?>> serdes) {
+        ExtensionProvidersLoader loader = new CustomExtensionProvidersLoader(serdes);
+        ExtensionsSupplier extensionsSupplier = () -> ExtensionProviders.createProvider(ExtensionSerDe.class, "network", loader);
+
+        return NetworkSerDe.read(getClass().getResourceAsStream("/extensionName_0_1_otherPrefix.xml"),
+                new ImportOptions(), null, NetworkFactory.findDefault(), extensionsSupplier, ReportNode.NO_OP);
+    }
+
+    private static class CustomExtensionProvidersLoader extends DefaultExtensionProvidersLoader {
+        private final List<ExtensionSerDe<?, ?>> serdes;
+
+        public CustomExtensionProvidersLoader(List<ExtensionSerDe<?, ?>> serdes) {
+            this.serdes = serdes;
+        }
+
+        @Override
+        public <T extends ExtensionProvider> Stream<T> getServicesStream(Class<T> clazz) {
+            return clazz == ExtensionSerDe.class ? serdes.stream().map(clazz::cast) :
+                    new ServiceLoaderCache<>(clazz).getServices().stream();
+        }
+    }
+
+    private static ExtensionSerDe<Load, LoadFooExt> createDummyExtensionSerDe() {
+        return new AbstractExtensionSerDe<>("loadElementMock", "network", LoadFooExt.class,
+                "", "", "") {
+            @Override
+            public void write(LoadFooExt extension, SerializerContext context) {
+                // Do nothing
+            }
+
+            @Override
+            public LoadFooExt read(Load extendable, DeserializerContext context) {
+                LoadFooExt extension = new LoadFooExt(extendable);
+                extendable.addExtension(LoadFooExt.class, extension);
+                return extension;
+            }
+        };
+    }
+
+    private static Network getNetwork() {
+        Network network = NetworkFactory.findDefault().createNetwork("test", "test");
+        network.setCaseDate(ZonedDateTime.parse("2024-09-17T13:36:37.831Z"));
+        Substation s1 = network.newSubstation().setId("S1").add();
+        VoltageLevel vl1 = s1.newVoltageLevel().setId("VL1").setNominalV(450).setTopologyKind(TopologyKind.NODE_BREAKER).add();
+        Load load1 = vl1.newLoad().setId("Load1").setNode(0).setP0(10.).setQ0(20.).add();
+        load1.addExtension(LoadMockExt.class, new LoadMockExt(load1));
+        return network;
+    }
+}
diff --git a/iidm/iidm-serde/src/test/resources/V1_0/xsd/loadMock_V0_1.xsd b/iidm/iidm-serde/src/test/resources/V1_0/xsd/loadMock_V0_1.xsd
new file mode 100644
index 00000000000..a2e1c33d7f9
--- /dev/null
+++ b/iidm/iidm-serde/src/test/resources/V1_0/xsd/loadMock_V0_1.xsd
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright (c) 2025, RTE (http://www.rte-france.com)
+    This Source Code Form is subject to the terms of the Mozilla Public
+    License, v. 2.0. If a copy of the MPL was not distributed with this
+    file, You can obtain one at http://mozilla.org/MPL/2.0/.
+    SPDX-License-Identifier: MPL-2.0
+
+-->
+<xs:schema version="1.0"
+           xmlns:xs="http://www.w3.org/2001/XMLSchema"
+           targetNamespace="http://www.powsybl.org/schema/iidm/ext/load_element_mock/1_0"
+           elementFormDefault="qualified">
+    <xs:element name="loadElementMock">
+        <xs:complexType/>
+    </xs:element>
+</xs:schema>
diff --git a/iidm/iidm-serde/src/test/resources/V1_0/xsd/loadMock_V0_2.xsd b/iidm/iidm-serde/src/test/resources/V1_0/xsd/loadMock_V0_2.xsd
new file mode 100644
index 00000000000..b9bbb5ba7fb
--- /dev/null
+++ b/iidm/iidm-serde/src/test/resources/V1_0/xsd/loadMock_V0_2.xsd
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright (c) 2025, RTE (http://www.rte-france.com)
+    This Source Code Form is subject to the terms of the Mozilla Public
+    License, v. 2.0. If a copy of the MPL was not distributed with this
+    file, You can obtain one at http://mozilla.org/MPL/2.0/.
+    SPDX-License-Identifier: MPL-2.0
+
+-->
+<xs:schema version="1.0"
+           xmlns:xs="http://www.w3.org/2001/XMLSchema"
+           targetNamespace="http://www.powsybl.org/schema/iidm/ext/load_element_mock/1_1"
+           elementFormDefault="qualified">
+    <xs:element name="loadEltMock">
+        <xs:complexType/>
+    </xs:element>
+</xs:schema>
diff --git a/iidm/iidm-serde/src/test/resources/extensionName_0_1.xml b/iidm/iidm-serde/src/test/resources/extensionName_0_1.xml
new file mode 100644
index 00000000000..3a7e54001f8
--- /dev/null
+++ b/iidm/iidm-serde/src/test/resources/extensionName_0_1.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<iidm:network xmlns:iidm="http://www.itesla_project.eu/schema/iidm/1_0" xmlns:lmock="http://www.powsybl.org/schema/iidm/ext/load_element_mock/1_0" id="test" caseDate="2024-09-17T13:36:37.831Z" forecastDistance="0" sourceFormat="test">
+    <iidm:substation id="S1">
+        <iidm:voltageLevel id="VL1" nominalV="450.0" topologyKind="NODE_BREAKER">
+            <iidm:nodeBreakerTopology nodeCount="1"/>
+            <iidm:load id="Load1" loadType="UNDEFINED" p0="10.0" q0="20.0" node="0"/>
+        </iidm:voltageLevel>
+    </iidm:substation>
+    <iidm:extension id="Load1">
+        <lmock:loadElementMock/>
+    </iidm:extension>
+</iidm:network>
\ No newline at end of file
diff --git a/iidm/iidm-serde/src/test/resources/extensionName_0_1_otherPrefix.xml b/iidm/iidm-serde/src/test/resources/extensionName_0_1_otherPrefix.xml
new file mode 100644
index 00000000000..b3941f69e3d
--- /dev/null
+++ b/iidm/iidm-serde/src/test/resources/extensionName_0_1_otherPrefix.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<iidm:network xmlns:iidm="http://www.itesla_project.eu/schema/iidm/1_0" xmlns:lemock="http://www.powsybl.org/schema/iidm/ext/load_element_mock/1_0" id="test" caseDate="2024-09-17T13:36:37.831Z" forecastDistance="0" sourceFormat="test">
+    <iidm:substation id="S1">
+        <iidm:voltageLevel id="VL1" nominalV="450.0" topologyKind="NODE_BREAKER">
+            <iidm:nodeBreakerTopology nodeCount="1"/>
+            <iidm:load id="Load1" loadType="UNDEFINED" p0="10.0" q0="20.0" node="0"/>
+        </iidm:voltageLevel>
+    </iidm:substation>
+    <iidm:extension id="Load1">
+        <lemock:loadElementMock/>
+    </iidm:extension>
+</iidm:network>
\ No newline at end of file
diff --git a/iidm/iidm-serde/src/test/resources/extensionName_0_2.xml b/iidm/iidm-serde/src/test/resources/extensionName_0_2.xml
new file mode 100644
index 00000000000..59ed0890a72
--- /dev/null
+++ b/iidm/iidm-serde/src/test/resources/extensionName_0_2.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<iidm:network xmlns:iidm="http://www.itesla_project.eu/schema/iidm/1_0" xmlns:lem="http://www.powsybl.org/schema/iidm/ext/load_element_mock/1_1" id="test" caseDate="2024-09-17T13:36:37.831Z" forecastDistance="0" sourceFormat="test">
+    <iidm:substation id="S1">
+        <iidm:voltageLevel id="VL1" nominalV="450.0" topologyKind="NODE_BREAKER">
+            <iidm:nodeBreakerTopology nodeCount="1"/>
+            <iidm:load id="Load1" loadType="UNDEFINED" p0="10.0" q0="20.0" node="0"/>
+        </iidm:voltageLevel>
+    </iidm:substation>
+    <iidm:extension id="Load1">
+        <lem:loadEltMock/>
+    </iidm:extension>
+</iidm:network>
\ No newline at end of file
diff --git a/iidm/iidm-serde/src/test/resources/extensionName_1_2.xml b/iidm/iidm-serde/src/test/resources/extensionName_1_2.xml
new file mode 100644
index 00000000000..bef4ac4f56e
--- /dev/null
+++ b/iidm/iidm-serde/src/test/resources/extensionName_1_2.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<iidm:network xmlns:iidm="http://www.powsybl.org/schema/iidm/1_1" xmlns:lmock="http://www.powsybl.org/schema/iidm/ext/load_mock/1_2" id="test" caseDate="2024-09-17T13:36:37.831Z" forecastDistance="0" sourceFormat="test">
+    <iidm:substation id="S1">
+        <iidm:voltageLevel id="VL1" nominalV="450.0" topologyKind="NODE_BREAKER">
+            <iidm:nodeBreakerTopology nodeCount="1"/>
+            <iidm:load id="Load1" loadType="UNDEFINED" p0="10.0" q0="20.0" node="0"/>
+        </iidm:voltageLevel>
+    </iidm:substation>
+    <iidm:extension id="Load1">
+        <lmock:loadMock/>
+    </iidm:extension>
+</iidm:network>
\ No newline at end of file
diff --git a/iidm/iidm-serde/src/test/resources/namespace-prefix-collision.xml b/iidm/iidm-serde/src/test/resources/namespace-prefix-collision.xml
new file mode 100644
index 00000000000..ea581bb2d1a
--- /dev/null
+++ b/iidm/iidm-serde/src/test/resources/namespace-prefix-collision.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<iidm:network xmlns:iidm="http://www.powsybl.org/schema/iidm/1_13" xmlns:bar="http://www.itesla_project.eu/schema/iidm/ext/loadbar/1_0" xmlns:foo="http://www.itesla_project.eu/schema/iidm/ext/loadfoo/1_0" xmlns:foo1="http://www.itesla_project.eu/schema/iidm/ext/voltagelevelfoo/1_0" id="test" caseDate="2017-11-17T12:00:00.000+01:00" forecastDistance="0" sourceFormat="test" minimumValidationLevel="STEADY_STATE_HYPOTHESIS">
+    <iidm:substation id="S" country="FR">
+        <iidm:voltageLevel id="VL" nominalV="20.0" lowVoltageLimit="15.0" highVoltageLimit="25.0" topologyKind="BUS_BREAKER">
+            <iidm:busBreakerTopology>
+                <iidm:bus id="BUS"/>
+            </iidm:busBreakerTopology>
+            <iidm:load id="LOAD" loadType="UNDEFINED" p0="0.0" q0="0.0" bus="BUS" connectableBus="BUS"/>
+            <iidm:load id="LOAD2" loadType="UNDEFINED" p0="0.0" q0="0.0" bus="BUS" connectableBus="BUS"/>
+        </iidm:voltageLevel>
+    </iidm:substation>
+    <iidm:extension id="LOAD">
+        <foo:loadFoo/>
+        <bar:loadBar/>
+    </iidm:extension>
+    <iidm:extension id="VL">
+        <foo1:voltageLevelFoo/>
+    </iidm:extension>
+    <iidm:extension id="LOAD2">
+        <foo:loadFoo/>
+    </iidm:extension>
+</iidm:network>
diff --git a/iidm/iidm-serde/src/test/resources/xsd/voltageLevelFoo.xsd b/iidm/iidm-serde/src/test/resources/xsd/voltageLevelFoo.xsd
new file mode 100644
index 00000000000..56a34ecc0fc
--- /dev/null
+++ b/iidm/iidm-serde/src/test/resources/xsd/voltageLevelFoo.xsd
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Copyright (c) 2025, RTE (http://www.rte-france.com)
+    This Source Code Form is subject to the terms of the Mozilla Public
+    License, v. 2.0. If a copy of the MPL was not distributed with this
+    file, You can obtain one at http://mozilla.org/MPL/2.0/.
+    SPDX-License-Identifier: MPL-2.0
+
+-->
+<xs:schema version="1.0"
+           xmlns:xs="http://www.w3.org/2001/XMLSchema"
+           targetNamespace="http://www.itesla_project.eu/schema/iidm/ext/voltagelevelfoo/1_0"
+           elementFormDefault="qualified">
+    <xs:element name="voltageLevelFoo">
+        <xs:complexType/>
+    </xs:element>
+</xs:schema>
diff --git a/iidm/iidm-test/src/main/java/com/powsybl/iidm/network/test/VoltageLevelFooExt.java b/iidm/iidm-test/src/main/java/com/powsybl/iidm/network/test/VoltageLevelFooExt.java
new file mode 100644
index 00000000000..934e1dc95e7
--- /dev/null
+++ b/iidm/iidm-test/src/main/java/com/powsybl/iidm/network/test/VoltageLevelFooExt.java
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.iidm.network.test;
+
+import com.powsybl.commons.extensions.AbstractExtension;
+import com.powsybl.iidm.network.VoltageLevel;
+
+/**
+ * @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>}
+ */
+public class VoltageLevelFooExt extends AbstractExtension<VoltageLevel> {
+
+    public VoltageLevelFooExt(VoltageLevel voltagelLevel) {
+        super(voltagelLevel);
+    }
+
+    @Override
+    public String getName() {
+        return "voltageLevelFoo";
+    }
+}


Reviews:
user: login: flo-dup
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-02-14T16:02:44Z
user: login: olperr1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-14T16:44:50Z
user: login: olperr1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-17T08:35:08Z
user: login: olperr1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-20T06:48:20Z
user: login: olperr1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-20T07:02:41Z
user: login: olperr1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-02-25T13:17:21Z
user: login: geofjamg
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-05T09:57:11Z
user: login: flo-dup
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T10:33:01Z
user: login: olperr1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-05T17:19:24Z
user: login: geofjamg
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-06T12:32:32Z
user: login: olperr1
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-06T13:07:17Z
user: login: flo-dup
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-06T16:05:53Z

Review comments:
diff_hunk: @@ -46,10 +51,56 @@ private ExtensionProviders(Class<T> clazz, String categoryName, Set<String> exte
         Objects.requireNonNull(clazz);
         Objects.requireNonNull(categoryName);
 
-        List<T> services = new ServiceLoaderCache<>(clazz).getServices();
-        providers = services.stream()
-                .filter(s -> s.getCategoryName().equals(categoryName) && (extensionNames == null || extensionNames.contains(s.getExtensionName())))
-                .collect(Collectors.toMap(T::getExtensionName, e -> e));
+        providers = loadProviders(clazz, categoryName, extensionNames);
+    }
+
+    private Map<String, T> loadProviders(Class<T> clazz, String categoryName, Set<String> extensionNames) {
+        final Map<String, T> providersMap = new HashMap<>();
+        ToBooleanBiFunction<String, String> validateNames = extensionNames == null ?
+                (n1, n2) -> true :
+                (n1, n2) -> extensionNames.contains(n1) || extensionNames.contains(n2);
+
+        Stream<T> servicesStream = getServicesStream(clazz);
+        if (categoryName != null) {
+            servicesStream = servicesStream.filter(s -> s.getCategoryName().equals(categoryName));
+        }
+        Set<T> services = servicesStream.collect(Collectors.toSet());
+        services.forEach(service -> addService(providersMap, service.getExtensionName(), service, validateNames));
+        if (clazz.equals(ExtensionSerDe.class)) {
+            // Add the alternative serialization names for extension SerDes
+            services.forEach(service -> ((ExtensionSerDe<?, ?>) service).getAlternativeSerializationNames()
+                    .forEach(name -> addService(providersMap, name, service, validateNames)));
+        }
+        return providersMap;
+    }
+
+    public static <T extends ExtensionProvider> Stream<T> getServicesStream(Class<T> clazz) {
+        return new ServiceLoaderCache<>(clazz).getServices().stream();
+    }
+
+    private void addService(Map<String, T> providersMap, String nameToAdd,
+                            T service, ToBooleanBiFunction<String, String> validateNames) {
+        if (validateNames.applyAsBoolean(nameToAdd, service.getExtensionName())) {
+            boolean add = true;
+            T previousService = providersMap.get(nameToAdd);
+            if (previousService != null) {
+                if (service.getExtensionName().equals(nameToAdd)) {
+                    // Historically, there was no control on possible duplicated service names
path: commons/src/main/java/com/powsybl/commons/extensions/ExtensionProviders.java
user: login: flo-dup
body: I think there was a control, as the `Collectors.toMap(keyMapper, valueMapper)` used throws an exception when encountering a duplicate key.
created_at: 2025-02-14T08:36:21Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,172 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.iidm.serde.extensions;
+
+import com.powsybl.commons.extensions.*;
+import com.powsybl.commons.io.DeserializerContext;
+import com.powsybl.commons.io.SerializerContext;
+import com.powsybl.commons.util.ServiceLoaderCache;
+import com.powsybl.iidm.network.*;
+import com.powsybl.iidm.network.test.LoadFooExt;
+import com.powsybl.iidm.network.test.LoadMockExt;
+import com.powsybl.iidm.serde.*;
+import com.powsybl.iidm.serde.extensions.util.NetworkSourceExtensionSerDe;
+import org.junit.jupiter.api.Test;
+import org.mockito.AdditionalMatchers;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.ZonedDateTime;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>}
+ */
+class SerializationNamesTest extends AbstractIidmSerDeTest {
+
+    public static final String LOAD_MOCK = "loadMock";
+    public static final String LOAD_ELEMENT_MOCK = "loadElementMock";
+
+    @Test
+    void unversionedExtensionTest() {
+        // Use the default extension version => also the default serialization name
+        AbstractExtensionSerDe<?, ?> serde = new NetworkSourceExtensionSerDe();
+        assertEquals(serde.getExtensionName(), serde.getSerializationName(serde.getVersion()));
+        Set<String> serializationNames = serde.getAlternativeSerializationNames();
+        assertTrue(serializationNames.isEmpty());
+    }
+
+    @Test
+    void getSerializationNameTest() {
+        // Use an extension version which serialization name is not the default
+        LoadMockSerDe serde = new LoadMockSerDe();
+        assertEquals(LOAD_MOCK, serde.getExtensionName());
+        assertEquals(LOAD_MOCK, serde.getSerializationName(serde.getVersion()));
+        assertEquals(LOAD_ELEMENT_MOCK, serde.getSerializationName("0.1"));
+        Set<String> serializationNames = serde.getAlternativeSerializationNames();
+        assertEquals(2, serializationNames.size());
+        assertTrue(serializationNames.containsAll(Set.of(LOAD_ELEMENT_MOCK, "loadEltMock")));
+    }
+
+    @Test
+    void realExtensionNameRoundTripTest() throws IOException {
+        // Use the default extension version => also the default serialization name
+        allFormatsRoundTripTest(getNetwork(), "/extensionName_1_2.xml", new ExportOptions().setVersion(IidmVersion.V_1_1.toString(".")));
+    }
+
+    @Test
+    void oldNameRoundTripTest() throws IOException {
+        // Use an extension version which serialization name is not the default
+        ExportOptions exportOptions = new ExportOptions()
+                .setVersion(IidmVersion.V_1_0.toString("."))
+                .addExtensionVersion("loadMock", "0.1");
+        allFormatsRoundTripTest(getNetwork(), "/extensionName_0_1.xml", exportOptions);
+    }
+
+    @Test
+    void importXiidmWithAnotherPrefixTest() throws URISyntaxException {
+        // Read a network with an old extension serialization name + a non-standard namespace prefix.
+        Network network = NetworkSerDe.validateAndRead(Paths.get(
+                Objects.requireNonNull(getClass().getResource("/extensionName_0_1_otherPrefix.xml")).toURI()));
+        assertNotNull(network.getLoad("Load1").getExtension(LoadMockExt.class));
+    }
+
+    @Test
+    void indicateExtensionAtImportTest() throws URISyntaxException {
+        // Read a network with an old extension serialization name
+        // To specify the extension to import, both the real extension name or the serialization name can be used.
+        Path file = Paths.get(Objects.requireNonNull(getClass().getResource("/extensionName_0_1_otherPrefix.xml")).toURI());
+        ImportOptions importOptions = new ImportOptions().addExtension("loadElementMock");
+        Network network = NetworkSerDe.read(file, importOptions);
+        assertNotNull(network.getLoad("Load1").getExtension(LoadMockExt.class),
+                "Using the serialization name as extension to load should work.");
+
+        importOptions = new ImportOptions().addExtension("loadMock");
+        network = NetworkSerDe.read(file, importOptions);
+        assertNotNull(network.getLoad("Load1").getExtension(LoadMockExt.class),
+                "Using the real extension name as extension to load should work.");
+    }
+
+    @Test
+    void ignoreOtherSerializationNameIfAlreadyUsed() throws URISyntaxException {
+        // No extension serde available
+        // This assertion is used to test that the configuration mechanism is working
+        Network network = loadNetworkWithGivenSerdes(List.of());
+        assertTrue(network.getLoad("Load1").getExtensions().isEmpty(), "No extension should be loaded.");
+
+        // The extension in the network ("loadElementMock") is only associated to the LoadMockExt extension,
+        // and it is defined as an "other serialization name"
+        List<ExtensionSerDe<?, ?>> serdes = List.of(new LoadMockSerDe());
+        network = loadNetworkWithGivenSerdes(serdes);
+        Load load1 = network.getLoad("Load1");
+        assertNotNull(load1.getExtension(LoadMockExt.class), "The read extension should be a LoadMockExt.");
+        assertEquals(1, load1.getExtensions().size(), "Only 1 extension should be loaded.");
+
+        // The extension in the network corresponds to the real extension name of the dummy extension SerDe
+        // and to an "other serialization name" of the LoadMockExt extension.
+        // In this case, the dummy extension should have the precedence (real names before "other" names)
+        // Note that the dummy SerDe loads a "LoadFooExt".
+        serdes = List.of(new LoadMockSerDe(), createDummyExtensionSerDe());
+        network = loadNetworkWithGivenSerdes(serdes);
+        load1 = network.getLoad("Load1");
+        assertNull(load1.getExtension(LoadMockExt.class), "No LoadMockExt should be loaded.");
+        assertNotNull(load1.getExtension(LoadFooExt.class), "A LoadFooExt extension should be loaded.");
+
+        // Reload the extensions supplier for the other tests
+        NetworkSerDe.reloadExtensionsSupplier();
path: iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/extensions/SerializationNamesTest.java
user: login: flo-dup
body: The two unit tests might be running in parallel :slightly_frowning_face: 
created_at: 2025-02-14T15:51:06Z
author_association: COLLABORATOR
diff_hunk: @@ -32,21 +33,61 @@ public abstract class AbstractVersionableNetworkExtensionSerDe<T extends Extenda
     private final String namespacePrefix;
     private final Map<IidmVersion, ImmutableSortedSet<String>> extensionVersions = new EnumMap<>(IidmVersion.class);
     private final BiMap<String, String> namespaceUris = HashBiMap.create();
+    private final Map<String, String> serializationNameByVersion = new HashMap<>();
+    private final Map<String, String> namespacePrefixByVersion = new HashMap<>();
+
+    public record AlternativeSerializationData(String name, Set<String> versions, String namespacePrefix) {
+        public AlternativeSerializationData(String name, Set<String> versions) {
+            this(name, versions, null);
+        }
+    }
 
     protected AbstractVersionableNetworkExtensionSerDe(String extensionName, Class<? super E> extensionClass, String namespacePrefix,
                                                        Map<IidmVersion, ImmutableSortedSet<String>> extensionVersions, Map<String, String> namespaceUris) {
+        this(extensionName, extensionClass, namespacePrefix, extensionVersions, namespaceUris, null);
+    }
+
+    protected AbstractVersionableNetworkExtensionSerDe(String extensionName, Class<? super E> extensionClass, String namespacePrefix,
+                                                       Map<IidmVersion, ImmutableSortedSet<String>> extensionVersions,
+                                                       Map<String, String> namespaceUris,
+                                                       Set<AlternativeSerializationData> alternativeSerializationData) {
path: iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/extensions/AbstractVersionableNetworkExtensionSerDe.java
user: login: flo-dup
body: rather a List?
created_at: 2025-02-14T15:55:51Z
author_association: COLLABORATOR
diff_hunk: @@ -32,21 +33,61 @@ public abstract class AbstractVersionableNetworkExtensionSerDe<T extends Extenda
     private final String namespacePrefix;
     private final Map<IidmVersion, ImmutableSortedSet<String>> extensionVersions = new EnumMap<>(IidmVersion.class);
     private final BiMap<String, String> namespaceUris = HashBiMap.create();
+    private final Map<String, String> serializationNameByVersion = new HashMap<>();
+    private final Map<String, String> namespacePrefixByVersion = new HashMap<>();
+
+    public record AlternativeSerializationData(String name, Set<String> versions, String namespacePrefix) {
+        public AlternativeSerializationData(String name, Set<String> versions) {
path: iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/extensions/AbstractVersionableNetworkExtensionSerDe.java
user: login: flo-dup
body: Maybe also a List here?
created_at: 2025-02-14T15:56:38Z
author_association: COLLABORATOR
diff_hunk: @@ -32,21 +33,61 @@ public abstract class AbstractVersionableNetworkExtensionSerDe<T extends Extenda
     private final String namespacePrefix;
     private final Map<IidmVersion, ImmutableSortedSet<String>> extensionVersions = new EnumMap<>(IidmVersion.class);
     private final BiMap<String, String> namespaceUris = HashBiMap.create();
+    private final Map<String, String> serializationNameByVersion = new HashMap<>();
+    private final Map<String, String> namespacePrefixByVersion = new HashMap<>();
+
+    public record AlternativeSerializationData(String name, Set<String> versions, String namespacePrefix) {
+        public AlternativeSerializationData(String name, Set<String> versions) {
+            this(name, versions, null);
+        }
+    }
 
     protected AbstractVersionableNetworkExtensionSerDe(String extensionName, Class<? super E> extensionClass, String namespacePrefix,
                                                        Map<IidmVersion, ImmutableSortedSet<String>> extensionVersions, Map<String, String> namespaceUris) {
+        this(extensionName, extensionClass, namespacePrefix, extensionVersions, namespaceUris, null);
+    }
+
+    protected AbstractVersionableNetworkExtensionSerDe(String extensionName, Class<? super E> extensionClass, String namespacePrefix,
path: iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/extensions/AbstractVersionableNetworkExtensionSerDe.java
user: login: flo-dup
body: It would be nice to specify everything based on the SerDe version instead of what's done currently, but that's another topic
created_at: 2025-02-14T15:57:38Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,172 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.iidm.serde.extensions;
+
+import com.powsybl.commons.extensions.*;
+import com.powsybl.commons.io.DeserializerContext;
+import com.powsybl.commons.io.SerializerContext;
+import com.powsybl.commons.util.ServiceLoaderCache;
+import com.powsybl.iidm.network.*;
+import com.powsybl.iidm.network.test.LoadFooExt;
+import com.powsybl.iidm.network.test.LoadMockExt;
+import com.powsybl.iidm.serde.*;
+import com.powsybl.iidm.serde.extensions.util.NetworkSourceExtensionSerDe;
+import org.junit.jupiter.api.Test;
+import org.mockito.AdditionalMatchers;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.ZonedDateTime;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>}
+ */
+class SerializationNamesTest extends AbstractIidmSerDeTest {
+
+    public static final String LOAD_MOCK = "loadMock";
+    public static final String LOAD_ELEMENT_MOCK = "loadElementMock";
+
+    @Test
+    void unversionedExtensionTest() {
+        // Use the default extension version => also the default serialization name
+        AbstractExtensionSerDe<?, ?> serde = new NetworkSourceExtensionSerDe();
+        assertEquals(serde.getExtensionName(), serde.getSerializationName(serde.getVersion()));
+        Set<String> serializationNames = serde.getAlternativeSerializationNames();
+        assertTrue(serializationNames.isEmpty());
+    }
+
+    @Test
+    void getSerializationNameTest() {
+        // Use an extension version which serialization name is not the default
+        LoadMockSerDe serde = new LoadMockSerDe();
+        assertEquals(LOAD_MOCK, serde.getExtensionName());
+        assertEquals(LOAD_MOCK, serde.getSerializationName(serde.getVersion()));
+        assertEquals(LOAD_ELEMENT_MOCK, serde.getSerializationName("0.1"));
+        Set<String> serializationNames = serde.getAlternativeSerializationNames();
+        assertEquals(2, serializationNames.size());
+        assertTrue(serializationNames.containsAll(Set.of(LOAD_ELEMENT_MOCK, "loadEltMock")));
+    }
+
+    @Test
+    void realExtensionNameRoundTripTest() throws IOException {
+        // Use the default extension version => also the default serialization name
+        allFormatsRoundTripTest(getNetwork(), "/extensionName_1_2.xml", new ExportOptions().setVersion(IidmVersion.V_1_1.toString(".")));
+    }
+
+    @Test
+    void oldNameRoundTripTest() throws IOException {
+        // Use an extension version which serialization name is not the default
+        ExportOptions exportOptions = new ExportOptions()
+                .setVersion(IidmVersion.V_1_0.toString("."))
+                .addExtensionVersion("loadMock", "0.1");
path: iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/extensions/SerializationNamesTest.java
user: login: flo-dup
body: Shouldn't we also allow to write `.addExtensionVersion("loadElementMock", "0.1")`?
created_at: 2025-02-14T16:00:26Z
author_association: COLLABORATOR
diff_hunk: @@ -46,10 +51,56 @@ private ExtensionProviders(Class<T> clazz, String categoryName, Set<String> exte
         Objects.requireNonNull(clazz);
         Objects.requireNonNull(categoryName);
 
-        List<T> services = new ServiceLoaderCache<>(clazz).getServices();
-        providers = services.stream()
-                .filter(s -> s.getCategoryName().equals(categoryName) && (extensionNames == null || extensionNames.contains(s.getExtensionName())))
-                .collect(Collectors.toMap(T::getExtensionName, e -> e));
+        providers = loadProviders(clazz, categoryName, extensionNames);
+    }
+
+    private Map<String, T> loadProviders(Class<T> clazz, String categoryName, Set<String> extensionNames) {
+        final Map<String, T> providersMap = new HashMap<>();
+        ToBooleanBiFunction<String, String> validateNames = extensionNames == null ?
+                (n1, n2) -> true :
+                (n1, n2) -> extensionNames.contains(n1) || extensionNames.contains(n2);
+
+        Stream<T> servicesStream = getServicesStream(clazz);
+        if (categoryName != null) {
+            servicesStream = servicesStream.filter(s -> s.getCategoryName().equals(categoryName));
+        }
+        Set<T> services = servicesStream.collect(Collectors.toSet());
+        services.forEach(service -> addService(providersMap, service.getExtensionName(), service, validateNames));
+        if (clazz.equals(ExtensionSerDe.class)) {
+            // Add the alternative serialization names for extension SerDes
+            services.forEach(service -> ((ExtensionSerDe<?, ?>) service).getAlternativeSerializationNames()
+                    .forEach(name -> addService(providersMap, name, service, validateNames)));
+        }
+        return providersMap;
+    }
+
+    public static <T extends ExtensionProvider> Stream<T> getServicesStream(Class<T> clazz) {
+        return new ServiceLoaderCache<>(clazz).getServices().stream();
+    }
+
+    private void addService(Map<String, T> providersMap, String nameToAdd,
+                            T service, ToBooleanBiFunction<String, String> validateNames) {
+        if (validateNames.applyAsBoolean(nameToAdd, service.getExtensionName())) {
+            boolean add = true;
+            T previousService = providersMap.get(nameToAdd);
+            if (previousService != null) {
+                if (service.getExtensionName().equals(nameToAdd)) {
+                    // Historically, there was no control on possible duplicated service names
path: commons/src/main/java/com/powsybl/commons/extensions/ExtensionProviders.java
user: login: olperr1
body: You are right, I missed that.
I pushed a change to keep the same behavior as previously. An `IllegalStateException` is thrown.
created_at: 2025-02-14T16:44:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,172 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.iidm.serde.extensions;
+
+import com.powsybl.commons.extensions.*;
+import com.powsybl.commons.io.DeserializerContext;
+import com.powsybl.commons.io.SerializerContext;
+import com.powsybl.commons.util.ServiceLoaderCache;
+import com.powsybl.iidm.network.*;
+import com.powsybl.iidm.network.test.LoadFooExt;
+import com.powsybl.iidm.network.test.LoadMockExt;
+import com.powsybl.iidm.serde.*;
+import com.powsybl.iidm.serde.extensions.util.NetworkSourceExtensionSerDe;
+import org.junit.jupiter.api.Test;
+import org.mockito.AdditionalMatchers;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.ZonedDateTime;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>}
+ */
+class SerializationNamesTest extends AbstractIidmSerDeTest {
+
+    public static final String LOAD_MOCK = "loadMock";
+    public static final String LOAD_ELEMENT_MOCK = "loadElementMock";
+
+    @Test
+    void unversionedExtensionTest() {
+        // Use the default extension version => also the default serialization name
+        AbstractExtensionSerDe<?, ?> serde = new NetworkSourceExtensionSerDe();
+        assertEquals(serde.getExtensionName(), serde.getSerializationName(serde.getVersion()));
+        Set<String> serializationNames = serde.getAlternativeSerializationNames();
+        assertTrue(serializationNames.isEmpty());
+    }
+
+    @Test
+    void getSerializationNameTest() {
+        // Use an extension version which serialization name is not the default
+        LoadMockSerDe serde = new LoadMockSerDe();
+        assertEquals(LOAD_MOCK, serde.getExtensionName());
+        assertEquals(LOAD_MOCK, serde.getSerializationName(serde.getVersion()));
+        assertEquals(LOAD_ELEMENT_MOCK, serde.getSerializationName("0.1"));
+        Set<String> serializationNames = serde.getAlternativeSerializationNames();
+        assertEquals(2, serializationNames.size());
+        assertTrue(serializationNames.containsAll(Set.of(LOAD_ELEMENT_MOCK, "loadEltMock")));
+    }
+
+    @Test
+    void realExtensionNameRoundTripTest() throws IOException {
+        // Use the default extension version => also the default serialization name
+        allFormatsRoundTripTest(getNetwork(), "/extensionName_1_2.xml", new ExportOptions().setVersion(IidmVersion.V_1_1.toString(".")));
+    }
+
+    @Test
+    void oldNameRoundTripTest() throws IOException {
+        // Use an extension version which serialization name is not the default
+        ExportOptions exportOptions = new ExportOptions()
+                .setVersion(IidmVersion.V_1_0.toString("."))
+                .addExtensionVersion("loadMock", "0.1");
+        allFormatsRoundTripTest(getNetwork(), "/extensionName_0_1.xml", exportOptions);
+    }
+
+    @Test
+    void importXiidmWithAnotherPrefixTest() throws URISyntaxException {
+        // Read a network with an old extension serialization name + a non-standard namespace prefix.
+        Network network = NetworkSerDe.validateAndRead(Paths.get(
+                Objects.requireNonNull(getClass().getResource("/extensionName_0_1_otherPrefix.xml")).toURI()));
+        assertNotNull(network.getLoad("Load1").getExtension(LoadMockExt.class));
+    }
+
+    @Test
+    void indicateExtensionAtImportTest() throws URISyntaxException {
+        // Read a network with an old extension serialization name
+        // To specify the extension to import, both the real extension name or the serialization name can be used.
+        Path file = Paths.get(Objects.requireNonNull(getClass().getResource("/extensionName_0_1_otherPrefix.xml")).toURI());
+        ImportOptions importOptions = new ImportOptions().addExtension("loadElementMock");
+        Network network = NetworkSerDe.read(file, importOptions);
+        assertNotNull(network.getLoad("Load1").getExtension(LoadMockExt.class),
+                "Using the serialization name as extension to load should work.");
+
+        importOptions = new ImportOptions().addExtension("loadMock");
+        network = NetworkSerDe.read(file, importOptions);
+        assertNotNull(network.getLoad("Load1").getExtension(LoadMockExt.class),
+                "Using the real extension name as extension to load should work.");
+    }
+
+    @Test
+    void ignoreOtherSerializationNameIfAlreadyUsed() throws URISyntaxException {
+        // No extension serde available
+        // This assertion is used to test that the configuration mechanism is working
+        Network network = loadNetworkWithGivenSerdes(List.of());
+        assertTrue(network.getLoad("Load1").getExtensions().isEmpty(), "No extension should be loaded.");
+
+        // The extension in the network ("loadElementMock") is only associated to the LoadMockExt extension,
+        // and it is defined as an "other serialization name"
+        List<ExtensionSerDe<?, ?>> serdes = List.of(new LoadMockSerDe());
+        network = loadNetworkWithGivenSerdes(serdes);
+        Load load1 = network.getLoad("Load1");
+        assertNotNull(load1.getExtension(LoadMockExt.class), "The read extension should be a LoadMockExt.");
+        assertEquals(1, load1.getExtensions().size(), "Only 1 extension should be loaded.");
+
+        // The extension in the network corresponds to the real extension name of the dummy extension SerDe
+        // and to an "other serialization name" of the LoadMockExt extension.
+        // In this case, the dummy extension should have the precedence (real names before "other" names)
+        // Note that the dummy SerDe loads a "LoadFooExt".
+        serdes = List.of(new LoadMockSerDe(), createDummyExtensionSerDe());
+        network = loadNetworkWithGivenSerdes(serdes);
+        load1 = network.getLoad("Load1");
+        assertNull(load1.getExtension(LoadMockExt.class), "No LoadMockExt should be loaded.");
+        assertNotNull(load1.getExtension(LoadFooExt.class), "A LoadFooExt extension should be loaded.");
+
+        // Reload the extensions supplier for the other tests
+        NetworkSerDe.reloadExtensionsSupplier();
path: iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/extensions/SerializationNamesTest.java
user: login: olperr1
body: I tried several ways to run them in // and I haven't encountered any problem:
- `mvn test -T1C`
- `mvn test -Dparallel=all -DperCoreThreadCount=true`

But yes, theoretically, this could append.

I can add these 2 annotations:
- `@NotThreadSafe`, [for the surefire // execution](https://maven.apache.org/surefire/maven-surefire-plugin/examples/fork-options-and-parallel-execution.html#parallel-test-execution-and-single-thread-execution);
- `@Execution(ExecutionMode.SAME_THREAD)`, [for the JUnit5 // execution](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution).

Also note that we haven't (yet?) configured JUnit5 to run in //. But adding the annotation can prevent problems later, if we activate it.
created_at: 2025-02-17T08:35:08Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,172 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.iidm.serde.extensions;
+
+import com.powsybl.commons.extensions.*;
+import com.powsybl.commons.io.DeserializerContext;
+import com.powsybl.commons.io.SerializerContext;
+import com.powsybl.commons.util.ServiceLoaderCache;
+import com.powsybl.iidm.network.*;
+import com.powsybl.iidm.network.test.LoadFooExt;
+import com.powsybl.iidm.network.test.LoadMockExt;
+import com.powsybl.iidm.serde.*;
+import com.powsybl.iidm.serde.extensions.util.NetworkSourceExtensionSerDe;
+import org.junit.jupiter.api.Test;
+import org.mockito.AdditionalMatchers;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.ZonedDateTime;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>}
+ */
+class SerializationNamesTest extends AbstractIidmSerDeTest {
+
+    public static final String LOAD_MOCK = "loadMock";
+    public static final String LOAD_ELEMENT_MOCK = "loadElementMock";
+
+    @Test
+    void unversionedExtensionTest() {
+        // Use the default extension version => also the default serialization name
+        AbstractExtensionSerDe<?, ?> serde = new NetworkSourceExtensionSerDe();
+        assertEquals(serde.getExtensionName(), serde.getSerializationName(serde.getVersion()));
+        Set<String> serializationNames = serde.getAlternativeSerializationNames();
+        assertTrue(serializationNames.isEmpty());
+    }
+
+    @Test
+    void getSerializationNameTest() {
+        // Use an extension version which serialization name is not the default
+        LoadMockSerDe serde = new LoadMockSerDe();
+        assertEquals(LOAD_MOCK, serde.getExtensionName());
+        assertEquals(LOAD_MOCK, serde.getSerializationName(serde.getVersion()));
+        assertEquals(LOAD_ELEMENT_MOCK, serde.getSerializationName("0.1"));
+        Set<String> serializationNames = serde.getAlternativeSerializationNames();
+        assertEquals(2, serializationNames.size());
+        assertTrue(serializationNames.containsAll(Set.of(LOAD_ELEMENT_MOCK, "loadEltMock")));
+    }
+
+    @Test
+    void realExtensionNameRoundTripTest() throws IOException {
+        // Use the default extension version => also the default serialization name
+        allFormatsRoundTripTest(getNetwork(), "/extensionName_1_2.xml", new ExportOptions().setVersion(IidmVersion.V_1_1.toString(".")));
+    }
+
+    @Test
+    void oldNameRoundTripTest() throws IOException {
+        // Use an extension version which serialization name is not the default
+        ExportOptions exportOptions = new ExportOptions()
+                .setVersion(IidmVersion.V_1_0.toString("."))
+                .addExtensionVersion("loadMock", "0.1");
path: iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/extensions/SerializationNamesTest.java
user: login: olperr1
body: I had the same question, but I think that it could add some complexity for the user because of potential serialization names duplications.
I will add a javadoc comment on the method to explain that the name expected here is the extension "real" name.
created_at: 2025-02-20T06:48:20Z
author_association: MEMBER
diff_hunk: @@ -32,21 +33,61 @@ public abstract class AbstractVersionableNetworkExtensionSerDe<T extends Extenda
     private final String namespacePrefix;
     private final Map<IidmVersion, ImmutableSortedSet<String>> extensionVersions = new EnumMap<>(IidmVersion.class);
     private final BiMap<String, String> namespaceUris = HashBiMap.create();
+    private final Map<String, String> serializationNameByVersion = new HashMap<>();
+    private final Map<String, String> namespacePrefixByVersion = new HashMap<>();
+
+    public record AlternativeSerializationData(String name, Set<String> versions, String namespacePrefix) {
+        public AlternativeSerializationData(String name, Set<String> versions) {
+            this(name, versions, null);
+        }
+    }
 
     protected AbstractVersionableNetworkExtensionSerDe(String extensionName, Class<? super E> extensionClass, String namespacePrefix,
                                                        Map<IidmVersion, ImmutableSortedSet<String>> extensionVersions, Map<String, String> namespaceUris) {
+        this(extensionName, extensionClass, namespacePrefix, extensionVersions, namespaceUris, null);
+    }
+
+    protected AbstractVersionableNetworkExtensionSerDe(String extensionName, Class<? super E> extensionClass, String namespacePrefix,
path: iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/extensions/AbstractVersionableNetworkExtensionSerDe.java
user: login: olperr1
body: You are right, it would be cleaner.
In this PR, I just wanted to add the new data without redundancy and without introducing breaking changes (since this feature is needed for a really few extensions).
But it is something we should work on.
created_at: 2025-02-20T07:02:41Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,172 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.iidm.serde.extensions;
+
+import com.powsybl.commons.extensions.*;
+import com.powsybl.commons.io.DeserializerContext;
+import com.powsybl.commons.io.SerializerContext;
+import com.powsybl.commons.util.ServiceLoaderCache;
+import com.powsybl.iidm.network.*;
+import com.powsybl.iidm.network.test.LoadFooExt;
+import com.powsybl.iidm.network.test.LoadMockExt;
+import com.powsybl.iidm.serde.*;
+import com.powsybl.iidm.serde.extensions.util.NetworkSourceExtensionSerDe;
+import org.junit.jupiter.api.Test;
+import org.mockito.AdditionalMatchers;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.ZonedDateTime;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>}
+ */
+class SerializationNamesTest extends AbstractIidmSerDeTest {
+
+    public static final String LOAD_MOCK = "loadMock";
+    public static final String LOAD_ELEMENT_MOCK = "loadElementMock";
+
+    @Test
+    void unversionedExtensionTest() {
+        // Use the default extension version => also the default serialization name
+        AbstractExtensionSerDe<?, ?> serde = new NetworkSourceExtensionSerDe();
+        assertEquals(serde.getExtensionName(), serde.getSerializationName(serde.getVersion()));
+        Set<String> serializationNames = serde.getAlternativeSerializationNames();
+        assertTrue(serializationNames.isEmpty());
+    }
+
+    @Test
+    void getSerializationNameTest() {
+        // Use an extension version which serialization name is not the default
+        LoadMockSerDe serde = new LoadMockSerDe();
+        assertEquals(LOAD_MOCK, serde.getExtensionName());
+        assertEquals(LOAD_MOCK, serde.getSerializationName(serde.getVersion()));
+        assertEquals(LOAD_ELEMENT_MOCK, serde.getSerializationName("0.1"));
+        Set<String> serializationNames = serde.getAlternativeSerializationNames();
+        assertEquals(2, serializationNames.size());
+        assertTrue(serializationNames.containsAll(Set.of(LOAD_ELEMENT_MOCK, "loadEltMock")));
+    }
+
+    @Test
+    void realExtensionNameRoundTripTest() throws IOException {
+        // Use the default extension version => also the default serialization name
+        allFormatsRoundTripTest(getNetwork(), "/extensionName_1_2.xml", new ExportOptions().setVersion(IidmVersion.V_1_1.toString(".")));
+    }
+
+    @Test
+    void oldNameRoundTripTest() throws IOException {
+        // Use an extension version which serialization name is not the default
+        ExportOptions exportOptions = new ExportOptions()
+                .setVersion(IidmVersion.V_1_0.toString("."))
+                .addExtensionVersion("loadMock", "0.1");
path: iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/extensions/SerializationNamesTest.java
user: login: olperr1
body: Javadoc comment added.
created_at: 2025-02-25T13:17:21Z
author_association: MEMBER
diff_hunk: @@ -248,13 +251,35 @@ private static XmlWriter createXmlWriter(Network n, OutputStream os, ExportOptio
             XmlWriter xmlWriter = new XmlWriter(os, indent, options.getCharset(), iidmNamespace, IIDM_PREFIX);
 
             Set<ExtensionSerDe<?, ?>> serializers = getExtensionSerializers(n, options);
+            Set<String> extensionUris = new HashSet<>();
+            Set<String> extensionPrefixes = new HashSet<>();
             for (ExtensionSerDe<?, ?> extensionSerDe : serializers) {
                 String extensionVersion = getExtensionVersion(extensionSerDe, options);
-                xmlWriter.setExtensionNamespace(extensionSerDe.getName(), extensionSerDe.getNamespaceUri(extensionVersion), extensionSerDe.getNamespacePrefix());
-            }
+                String namespaceUri = extensionSerDe.getNamespaceUri(extensionVersion);
+                String realNamespacePrefix = extensionSerDe.getNamespacePrefix(extensionVersion);
+                String fixedNamespacePrefix = realNamespacePrefix;
+
+                // Throw an exception if a namespace URI collision is detected
+                if (extensionUris.contains(namespaceUri)) {
+                    throw new PowsyblException("Extension namespace URI collision");
+                } else {
+                    extensionUris.add(namespaceUri);
+                }
 
-            // Ensure that there is no conflict in namespace prefixes and URIs
-            checkNamespaceCollisions(options, serializers);
+                // Try to compute another namespace prefix if a collision is detected
+                int i = 1;
+                while (i < 100 && extensionPrefixes.contains(fixedNamespacePrefix)) {
path: iidm/iidm-serde/src/main/java/com/powsybl/iidm/serde/NetworkSerDe.java
user: login: geofjamg
body: Could create a constant for that ?
created_at: 2025-03-05T09:48:27Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,172 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.iidm.serde.extensions;
+
+import com.powsybl.commons.extensions.*;
+import com.powsybl.commons.io.DeserializerContext;
+import com.powsybl.commons.io.SerializerContext;
+import com.powsybl.commons.util.ServiceLoaderCache;
+import com.powsybl.iidm.network.*;
+import com.powsybl.iidm.network.test.LoadFooExt;
+import com.powsybl.iidm.network.test.LoadMockExt;
+import com.powsybl.iidm.serde.*;
+import com.powsybl.iidm.serde.extensions.util.NetworkSourceExtensionSerDe;
+import org.junit.jupiter.api.Test;
+import org.mockito.AdditionalMatchers;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.ZonedDateTime;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>}
+ */
+class SerializationNamesTest extends AbstractIidmSerDeTest {
+
+    public static final String LOAD_MOCK = "loadMock";
+    public static final String LOAD_ELEMENT_MOCK = "loadElementMock";
+
+    @Test
+    void unversionedExtensionTest() {
+        // Use the default extension version => also the default serialization name
+        AbstractExtensionSerDe<?, ?> serde = new NetworkSourceExtensionSerDe();
+        assertEquals(serde.getExtensionName(), serde.getSerializationName(serde.getVersion()));
+        Set<String> serializationNames = serde.getAlternativeSerializationNames();
+        assertTrue(serializationNames.isEmpty());
+    }
+
+    @Test
+    void getSerializationNameTest() {
+        // Use an extension version which serialization name is not the default
+        LoadMockSerDe serde = new LoadMockSerDe();
+        assertEquals(LOAD_MOCK, serde.getExtensionName());
+        assertEquals(LOAD_MOCK, serde.getSerializationName(serde.getVersion()));
+        assertEquals(LOAD_ELEMENT_MOCK, serde.getSerializationName("0.1"));
+        Set<String> serializationNames = serde.getAlternativeSerializationNames();
+        assertEquals(2, serializationNames.size());
+        assertTrue(serializationNames.containsAll(Set.of(LOAD_ELEMENT_MOCK, "loadEltMock")));
+    }
+
+    @Test
+    void realExtensionNameRoundTripTest() throws IOException {
+        // Use the default extension version => also the default serialization name
+        allFormatsRoundTripTest(getNetwork(), "/extensionName_1_2.xml", new ExportOptions().setVersion(IidmVersion.V_1_1.toString(".")));
+    }
+
+    @Test
+    void oldNameRoundTripTest() throws IOException {
+        // Use an extension version which serialization name is not the default
+        ExportOptions exportOptions = new ExportOptions()
+                .setVersion(IidmVersion.V_1_0.toString("."))
+                .addExtensionVersion("loadMock", "0.1");
+        allFormatsRoundTripTest(getNetwork(), "/extensionName_0_1.xml", exportOptions);
+    }
+
+    @Test
+    void importXiidmWithAnotherPrefixTest() throws URISyntaxException {
+        // Read a network with an old extension serialization name + a non-standard namespace prefix.
+        Network network = NetworkSerDe.validateAndRead(Paths.get(
+                Objects.requireNonNull(getClass().getResource("/extensionName_0_1_otherPrefix.xml")).toURI()));
+        assertNotNull(network.getLoad("Load1").getExtension(LoadMockExt.class));
+    }
+
+    @Test
+    void indicateExtensionAtImportTest() throws URISyntaxException {
+        // Read a network with an old extension serialization name
+        // To specify the extension to import, both the real extension name or the serialization name can be used.
+        Path file = Paths.get(Objects.requireNonNull(getClass().getResource("/extensionName_0_1_otherPrefix.xml")).toURI());
path: iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/extensions/SerializationNamesTest.java
user: login: geofjamg
body: It is not robust to convert a resource to a Path. You should use `getResourceAsInputStream` and load the network from it.
created_at: 2025-03-05T09:51:01Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,172 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.iidm.serde.extensions;
+
+import com.powsybl.commons.extensions.*;
+import com.powsybl.commons.io.DeserializerContext;
+import com.powsybl.commons.io.SerializerContext;
+import com.powsybl.commons.util.ServiceLoaderCache;
+import com.powsybl.iidm.network.*;
+import com.powsybl.iidm.network.test.LoadFooExt;
+import com.powsybl.iidm.network.test.LoadMockExt;
+import com.powsybl.iidm.serde.*;
+import com.powsybl.iidm.serde.extensions.util.NetworkSourceExtensionSerDe;
+import org.junit.jupiter.api.Test;
+import org.mockito.AdditionalMatchers;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.ZonedDateTime;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>}
+ */
+class SerializationNamesTest extends AbstractIidmSerDeTest {
+
+    public static final String LOAD_MOCK = "loadMock";
+    public static final String LOAD_ELEMENT_MOCK = "loadElementMock";
+
+    @Test
+    void unversionedExtensionTest() {
+        // Use the default extension version => also the default serialization name
+        AbstractExtensionSerDe<?, ?> serde = new NetworkSourceExtensionSerDe();
+        assertEquals(serde.getExtensionName(), serde.getSerializationName(serde.getVersion()));
+        Set<String> serializationNames = serde.getAlternativeSerializationNames();
+        assertTrue(serializationNames.isEmpty());
+    }
+
+    @Test
+    void getSerializationNameTest() {
+        // Use an extension version which serialization name is not the default
+        LoadMockSerDe serde = new LoadMockSerDe();
+        assertEquals(LOAD_MOCK, serde.getExtensionName());
+        assertEquals(LOAD_MOCK, serde.getSerializationName(serde.getVersion()));
+        assertEquals(LOAD_ELEMENT_MOCK, serde.getSerializationName("0.1"));
+        Set<String> serializationNames = serde.getAlternativeSerializationNames();
+        assertEquals(2, serializationNames.size());
+        assertTrue(serializationNames.containsAll(Set.of(LOAD_ELEMENT_MOCK, "loadEltMock")));
+    }
+
+    @Test
+    void realExtensionNameRoundTripTest() throws IOException {
+        // Use the default extension version => also the default serialization name
+        allFormatsRoundTripTest(getNetwork(), "/extensionName_1_2.xml", new ExportOptions().setVersion(IidmVersion.V_1_1.toString(".")));
+    }
+
+    @Test
+    void oldNameRoundTripTest() throws IOException {
+        // Use an extension version which serialization name is not the default
+        ExportOptions exportOptions = new ExportOptions()
+                .setVersion(IidmVersion.V_1_0.toString("."))
+                .addExtensionVersion("loadMock", "0.1");
+        allFormatsRoundTripTest(getNetwork(), "/extensionName_0_1.xml", exportOptions);
+    }
+
+    @Test
+    void importXiidmWithAnotherPrefixTest() throws URISyntaxException {
+        // Read a network with an old extension serialization name + a non-standard namespace prefix.
+        Network network = NetworkSerDe.validateAndRead(Paths.get(
path: iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/extensions/SerializationNamesTest.java
user: login: geofjamg
body: Same
created_at: 2025-03-05T09:51:12Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,172 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.iidm.serde.extensions;
+
+import com.powsybl.commons.extensions.*;
+import com.powsybl.commons.io.DeserializerContext;
+import com.powsybl.commons.io.SerializerContext;
+import com.powsybl.commons.util.ServiceLoaderCache;
+import com.powsybl.iidm.network.*;
+import com.powsybl.iidm.network.test.LoadFooExt;
+import com.powsybl.iidm.network.test.LoadMockExt;
+import com.powsybl.iidm.serde.*;
+import com.powsybl.iidm.serde.extensions.util.NetworkSourceExtensionSerDe;
+import org.junit.jupiter.api.Test;
+import org.mockito.AdditionalMatchers;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.ZonedDateTime;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>}
+ */
+class SerializationNamesTest extends AbstractIidmSerDeTest {
+
+    public static final String LOAD_MOCK = "loadMock";
+    public static final String LOAD_ELEMENT_MOCK = "loadElementMock";
+
+    @Test
+    void unversionedExtensionTest() {
+        // Use the default extension version => also the default serialization name
+        AbstractExtensionSerDe<?, ?> serde = new NetworkSourceExtensionSerDe();
+        assertEquals(serde.getExtensionName(), serde.getSerializationName(serde.getVersion()));
+        Set<String> serializationNames = serde.getAlternativeSerializationNames();
+        assertTrue(serializationNames.isEmpty());
+    }
+
+    @Test
+    void getSerializationNameTest() {
+        // Use an extension version which serialization name is not the default
+        LoadMockSerDe serde = new LoadMockSerDe();
+        assertEquals(LOAD_MOCK, serde.getExtensionName());
+        assertEquals(LOAD_MOCK, serde.getSerializationName(serde.getVersion()));
+        assertEquals(LOAD_ELEMENT_MOCK, serde.getSerializationName("0.1"));
+        Set<String> serializationNames = serde.getAlternativeSerializationNames();
+        assertEquals(2, serializationNames.size());
+        assertTrue(serializationNames.containsAll(Set.of(LOAD_ELEMENT_MOCK, "loadEltMock")));
+    }
+
+    @Test
+    void realExtensionNameRoundTripTest() throws IOException {
+        // Use the default extension version => also the default serialization name
+        allFormatsRoundTripTest(getNetwork(), "/extensionName_1_2.xml", new ExportOptions().setVersion(IidmVersion.V_1_1.toString(".")));
+    }
+
+    @Test
+    void oldNameRoundTripTest() throws IOException {
+        // Use an extension version which serialization name is not the default
+        ExportOptions exportOptions = new ExportOptions()
+                .setVersion(IidmVersion.V_1_0.toString("."))
+                .addExtensionVersion("loadMock", "0.1");
+        allFormatsRoundTripTest(getNetwork(), "/extensionName_0_1.xml", exportOptions);
+    }
+
+    @Test
+    void importXiidmWithAnotherPrefixTest() throws URISyntaxException {
+        // Read a network with an old extension serialization name + a non-standard namespace prefix.
+        Network network = NetworkSerDe.validateAndRead(Paths.get(
+                Objects.requireNonNull(getClass().getResource("/extensionName_0_1_otherPrefix.xml")).toURI()));
+        assertNotNull(network.getLoad("Load1").getExtension(LoadMockExt.class));
+    }
+
+    @Test
+    void indicateExtensionAtImportTest() throws URISyntaxException {
+        // Read a network with an old extension serialization name
+        // To specify the extension to import, both the real extension name or the serialization name can be used.
+        Path file = Paths.get(Objects.requireNonNull(getClass().getResource("/extensionName_0_1_otherPrefix.xml")).toURI());
+        ImportOptions importOptions = new ImportOptions().addExtension("loadElementMock");
+        Network network = NetworkSerDe.read(file, importOptions);
+        assertNotNull(network.getLoad("Load1").getExtension(LoadMockExt.class),
+                "Using the serialization name as extension to load should work.");
+
+        importOptions = new ImportOptions().addExtension("loadMock");
+        network = NetworkSerDe.read(file, importOptions);
+        assertNotNull(network.getLoad("Load1").getExtension(LoadMockExt.class),
+                "Using the real extension name as extension to load should work.");
+    }
+
+    @Test
+    void ignoreOtherSerializationNameIfAlreadyUsed() throws URISyntaxException {
+        // No extension serde available
+        // This assertion is used to test that the configuration mechanism is working
+        Network network = loadNetworkWithGivenSerdes(List.of());
+        assertTrue(network.getLoad("Load1").getExtensions().isEmpty(), "No extension should be loaded.");
+
+        // The extension in the network ("loadElementMock") is only associated to the LoadMockExt extension,
+        // and it is defined as an "other serialization name"
+        List<ExtensionSerDe<?, ?>> serdes = List.of(new LoadMockSerDe());
+        network = loadNetworkWithGivenSerdes(serdes);
+        Load load1 = network.getLoad("Load1");
+        assertNotNull(load1.getExtension(LoadMockExt.class), "The read extension should be a LoadMockExt.");
+        assertEquals(1, load1.getExtensions().size(), "Only 1 extension should be loaded.");
+
+        // The extension in the network corresponds to the real extension name of the dummy extension SerDe
+        // and to an "other serialization name" of the LoadMockExt extension.
+        // In this case, the dummy extension should have the precedence (real names before "other" names)
+        // Note that the dummy SerDe loads a "LoadFooExt".
+        serdes = List.of(new LoadMockSerDe(), createDummyExtensionSerDe());
+        network = loadNetworkWithGivenSerdes(serdes);
+        load1 = network.getLoad("Load1");
+        assertNull(load1.getExtension(LoadMockExt.class), "No LoadMockExt should be loaded.");
+        assertNotNull(load1.getExtension(LoadFooExt.class), "A LoadFooExt extension should be loaded.");
+
+        // Reload the extensions supplier for the other tests
+        NetworkSerDe.reloadExtensionsSupplier();
path: iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/extensions/SerializationNamesTest.java
user: login: geofjamg
body: Static mocking is the hell. We should never use it.
We also should avoid using a service loader implementation on test side because service loader is kind of global configuration with prevent for testing different cases unless hacking with such static mocking and reloading.
There is a good example in `Network.load()` and `ImportersLoader` to how we can tackle this issue and a safer way, by bypassing the service loader on test side and using an explicit list of services.
created_at: 2025-03-05T09:56:43Z
author_association: MEMBER
diff_hunk: @@ -46,10 +51,51 @@ private ExtensionProviders(Class<T> clazz, String categoryName, Set<String> exte
         Objects.requireNonNull(clazz);
         Objects.requireNonNull(categoryName);
 
-        List<T> services = new ServiceLoaderCache<>(clazz).getServices();
-        providers = services.stream()
-                .filter(s -> s.getCategoryName().equals(categoryName) && (extensionNames == null || extensionNames.contains(s.getExtensionName())))
-                .collect(Collectors.toMap(T::getExtensionName, e -> e));
+        providers = loadProviders(clazz, categoryName, extensionNames);
+    }
+
+    private Map<String, T> loadProviders(Class<T> clazz, String categoryName, Set<String> extensionNames) {
+        final Map<String, T> providersMap = new HashMap<>();
+        ToBooleanBiFunction<String, String> validateNames = extensionNames == null ?
+                (n1, n2) -> true :
+                (n1, n2) -> extensionNames.contains(n1) || extensionNames.contains(n2);
+
+        Stream<T> servicesStream = getServicesStream(clazz);
+        if (categoryName != null) {
+            servicesStream = servicesStream.filter(s -> s.getCategoryName().equals(categoryName));
+        }
+        Set<T> services = servicesStream.collect(Collectors.toSet());
+        services.forEach(service -> addService(providersMap, service.getExtensionName(), service, validateNames));
+        if (clazz.equals(ExtensionSerDe.class)) {
+            // Add the alternative serialization names for extension SerDes
+            services.forEach(service -> ((ExtensionSerDe<?, ?>) service).getAlternativeSerializationNames()
+                    .forEach(name -> addService(providersMap, name, service, validateNames)));
+        }
path: commons/src/main/java/com/powsybl/commons/extensions/ExtensionProviders.java
user: login: flo-dup
body: I think that putting in a specific class extending ExtensionProviders will keep the base class clearer than now. Without a ToBooleanBiFunction which in the general case is called with n1 == n2, without a distinct() in getProviders which in the general case is not useful.
created_at: 2025-03-05T10:30:01Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,172 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.iidm.serde.extensions;
+
+import com.powsybl.commons.extensions.*;
+import com.powsybl.commons.io.DeserializerContext;
+import com.powsybl.commons.io.SerializerContext;
+import com.powsybl.commons.util.ServiceLoaderCache;
+import com.powsybl.iidm.network.*;
+import com.powsybl.iidm.network.test.LoadFooExt;
+import com.powsybl.iidm.network.test.LoadMockExt;
+import com.powsybl.iidm.serde.*;
+import com.powsybl.iidm.serde.extensions.util.NetworkSourceExtensionSerDe;
+import org.junit.jupiter.api.Test;
+import org.mockito.AdditionalMatchers;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.ZonedDateTime;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * @author Olivier Perrin {@literal <olivier.perrin at rte-france.com>}
+ */
+class SerializationNamesTest extends AbstractIidmSerDeTest {
+
+    public static final String LOAD_MOCK = "loadMock";
+    public static final String LOAD_ELEMENT_MOCK = "loadElementMock";
+
+    @Test
+    void unversionedExtensionTest() {
+        // Use the default extension version => also the default serialization name
+        AbstractExtensionSerDe<?, ?> serde = new NetworkSourceExtensionSerDe();
+        assertEquals(serde.getExtensionName(), serde.getSerializationName(serde.getVersion()));
+        Set<String> serializationNames = serde.getAlternativeSerializationNames();
+        assertTrue(serializationNames.isEmpty());
+    }
+
+    @Test
+    void getSerializationNameTest() {
+        // Use an extension version which serialization name is not the default
+        LoadMockSerDe serde = new LoadMockSerDe();
+        assertEquals(LOAD_MOCK, serde.getExtensionName());
+        assertEquals(LOAD_MOCK, serde.getSerializationName(serde.getVersion()));
+        assertEquals(LOAD_ELEMENT_MOCK, serde.getSerializationName("0.1"));
+        Set<String> serializationNames = serde.getAlternativeSerializationNames();
+        assertEquals(2, serializationNames.size());
+        assertTrue(serializationNames.containsAll(Set.of(LOAD_ELEMENT_MOCK, "loadEltMock")));
+    }
+
+    @Test
+    void realExtensionNameRoundTripTest() throws IOException {
+        // Use the default extension version => also the default serialization name
+        allFormatsRoundTripTest(getNetwork(), "/extensionName_1_2.xml", new ExportOptions().setVersion(IidmVersion.V_1_1.toString(".")));
+    }
+
+    @Test
+    void oldNameRoundTripTest() throws IOException {
+        // Use an extension version which serialization name is not the default
+        ExportOptions exportOptions = new ExportOptions()
+                .setVersion(IidmVersion.V_1_0.toString("."))
+                .addExtensionVersion("loadMock", "0.1");
+        allFormatsRoundTripTest(getNetwork(), "/extensionName_0_1.xml", exportOptions);
+    }
+
+    @Test
+    void importXiidmWithAnotherPrefixTest() throws URISyntaxException {
+        // Read a network with an old extension serialization name + a non-standard namespace prefix.
+        Network network = NetworkSerDe.validateAndRead(Paths.get(
+                Objects.requireNonNull(getClass().getResource("/extensionName_0_1_otherPrefix.xml")).toURI()));
+        assertNotNull(network.getLoad("Load1").getExtension(LoadMockExt.class));
+    }
+
+    @Test
+    void indicateExtensionAtImportTest() throws URISyntaxException {
+        // Read a network with an old extension serialization name
+        // To specify the extension to import, both the real extension name or the serialization name can be used.
+        Path file = Paths.get(Objects.requireNonNull(getClass().getResource("/extensionName_0_1_otherPrefix.xml")).toURI());
+        ImportOptions importOptions = new ImportOptions().addExtension("loadElementMock");
+        Network network = NetworkSerDe.read(file, importOptions);
+        assertNotNull(network.getLoad("Load1").getExtension(LoadMockExt.class),
+                "Using the serialization name as extension to load should work.");
+
+        importOptions = new ImportOptions().addExtension("loadMock");
+        network = NetworkSerDe.read(file, importOptions);
+        assertNotNull(network.getLoad("Load1").getExtension(LoadMockExt.class),
+                "Using the real extension name as extension to load should work.");
+    }
+
+    @Test
+    void ignoreOtherSerializationNameIfAlreadyUsed() throws URISyntaxException {
+        // No extension serde available
+        // This assertion is used to test that the configuration mechanism is working
+        Network network = loadNetworkWithGivenSerdes(List.of());
+        assertTrue(network.getLoad("Load1").getExtensions().isEmpty(), "No extension should be loaded.");
+
+        // The extension in the network ("loadElementMock") is only associated to the LoadMockExt extension,
+        // and it is defined as an "other serialization name"
+        List<ExtensionSerDe<?, ?>> serdes = List.of(new LoadMockSerDe());
+        network = loadNetworkWithGivenSerdes(serdes);
+        Load load1 = network.getLoad("Load1");
+        assertNotNull(load1.getExtension(LoadMockExt.class), "The read extension should be a LoadMockExt.");
+        assertEquals(1, load1.getExtensions().size(), "Only 1 extension should be loaded.");
+
+        // The extension in the network corresponds to the real extension name of the dummy extension SerDe
+        // and to an "other serialization name" of the LoadMockExt extension.
+        // In this case, the dummy extension should have the precedence (real names before "other" names)
+        // Note that the dummy SerDe loads a "LoadFooExt".
+        serdes = List.of(new LoadMockSerDe(), createDummyExtensionSerDe());
+        network = loadNetworkWithGivenSerdes(serdes);
+        load1 = network.getLoad("Load1");
+        assertNull(load1.getExtension(LoadMockExt.class), "No LoadMockExt should be loaded.");
+        assertNotNull(load1.getExtension(LoadFooExt.class), "A LoadFooExt extension should be loaded.");
+
+        // Reload the extensions supplier for the other tests
+        NetworkSerDe.reloadExtensionsSupplier();
path: iidm/iidm-serde/src/test/java/com/powsybl/iidm/serde/extensions/SerializationNamesTest.java
user: login: olperr1
body: I removed the static mocking in b7c150d174f20ff25bc5f95d79c602c710d22f99.
created_at: 2025-03-05T17:19:24Z
author_association: MEMBER
diff_hunk: @@ -46,10 +51,51 @@ private ExtensionProviders(Class<T> clazz, String categoryName, Set<String> exte
         Objects.requireNonNull(clazz);
         Objects.requireNonNull(categoryName);
 
-        List<T> services = new ServiceLoaderCache<>(clazz).getServices();
-        providers = services.stream()
-                .filter(s -> s.getCategoryName().equals(categoryName) && (extensionNames == null || extensionNames.contains(s.getExtensionName())))
-                .collect(Collectors.toMap(T::getExtensionName, e -> e));
+        providers = loadProviders(clazz, categoryName, extensionNames);
+    }
+
+    private Map<String, T> loadProviders(Class<T> clazz, String categoryName, Set<String> extensionNames) {
+        final Map<String, T> providersMap = new HashMap<>();
+        ToBooleanBiFunction<String, String> validateNames = extensionNames == null ?
+                (n1, n2) -> true :
+                (n1, n2) -> extensionNames.contains(n1) || extensionNames.contains(n2);
+
+        Stream<T> servicesStream = getServicesStream(clazz);
+        if (categoryName != null) {
+            servicesStream = servicesStream.filter(s -> s.getCategoryName().equals(categoryName));
+        }
+        Set<T> services = servicesStream.collect(Collectors.toSet());
+        services.forEach(service -> addService(providersMap, service.getExtensionName(), service, validateNames));
+        if (clazz.equals(ExtensionSerDe.class)) {
+            // Add the alternative serialization names for extension SerDes
+            services.forEach(service -> ((ExtensionSerDe<?, ?>) service).getAlternativeSerializationNames()
+                    .forEach(name -> addService(providersMap, name, service, validateNames)));
+        }
path: commons/src/main/java/com/powsybl/commons/extensions/ExtensionProviders.java
user: login: olperr1
body: I split `addService` in 2 for the 2 cases (registering the provider to its extension name and to an alternative name).
The `ToBooleanBiFunction` was therefore removed.
(See b0344a951094dae818c2ef49e6e37fff5b03c575)

I did not introduce inheritance because the class is actually static final. This would thus be an heavy change (and a breaking one).
created_at: 2025-03-06T13:07:17Z
author_association: MEMBER

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-06T16:16:58Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=com.powsybl%3Apowsybl-core&pullRequest=3282) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [6 New issues](https://sonarcloud.io/project/issues?id=com.powsybl%3Apowsybl-core&pullRequest=3282&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=com.powsybl%3Apowsybl-core&pullRequest=3282&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=com.powsybl%3Apowsybl-core&pullRequest=3282&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [90.1% Coverage on New Code](https://sonarcloud.io/component_measures?id=com.powsybl%3Apowsybl-core&pullRequest=3282&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=com.powsybl%3Apowsybl-core&pullRequest=3282&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=com.powsybl%3Apowsybl-core&pullRequest=3282)


author_association: NONE