Index: /trunk/src/org/openstreetmap/josm/tools/StringParser.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/StringParser.java	(revision 16181)
+++ /trunk/src/org/openstreetmap/josm/tools/StringParser.java	(revision 16181)
@@ -0,0 +1,114 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+
+/**
+ * Utility class to parse various types from strings.
+ *
+ * @since 16181
+ */
+public class StringParser {
+
+    /**
+     * The default instance supports parsing {@link String}, {@link Character}, {@link Boolean},
+     * {@link Byte}, {@link Short}, {@link Integer}, {@link Long}, {@link Float}, {@link Double} (in their primitive and boxed form).
+     */
+    public static final StringParser DEFAULT = new StringParser(Utils.toUnmodifiableMap(new StringParser()
+            .registerParser(String.class, Function.identity())
+            .registerParser(char.class, value -> value.charAt(0))
+            .registerParser(Character.class, value -> value.charAt(0))
+            .registerParser(boolean.class, Boolean::parseBoolean)
+            .registerParser(Boolean.class, Boolean::parseBoolean)
+            .registerParser(byte.class, Byte::parseByte)
+            .registerParser(Byte.class, Byte::parseByte)
+            .registerParser(short.class, Short::parseShort)
+            .registerParser(Short.class, Short::parseShort)
+            .registerParser(int.class, Integer::parseInt)
+            .registerParser(Integer.class, Integer::parseInt)
+            .registerParser(long.class, Long::parseLong)
+            .registerParser(Long.class, Long::parseLong)
+            .registerParser(float.class, Float::parseFloat)
+            .registerParser(Float.class, Float::parseFloat)
+            .registerParser(double.class, Double::parseDouble)
+            .registerParser(Double.class, Double::parseDouble)
+            .parsers));
+
+    private final Map<Class<?>, Function<String, ?>> parsers;
+
+    public StringParser() {
+        this(new LinkedHashMap<>());
+    }
+
+    /**
+     * Creates a new {@code StringParser} by deeply copying {@code parser}
+     *
+     * @param parser the parser to copy
+     */
+    public StringParser(StringParser parser) {
+        this(new LinkedHashMap<>(parser.parsers));
+    }
+
+    private StringParser(Map<Class<?>, Function<String, ?>> parsers) {
+        this.parsers = parsers;
+    }
+
+    public <T> StringParser registerParser(Class<T> type, Function<String, T> value) {
+        parsers.put(type, value);
+        return this;
+    }
+
+    /**
+     * Determines whether {@code type} can be {@linkplain #parse parsed}
+     *
+     * @param type the type
+     * @return true if {@code type} can be parsed
+     */
+    public boolean supports(Class<?> type) {
+        return parsers.containsKey(type);
+    }
+
+    /**
+     * Parses the given {@code string} as {@code type} and returns the result
+     *
+     * @param type   the type class
+     * @param string the string to parse
+     * @param <T>    the type
+     * @return the parsed value for {@code string} as type {@code type}
+     * @throws UnsupportedOperationException if {@code type} is not {@linkplain #supports supported}
+     * @throws UncheckedParseException       when the parsing fails
+     */
+    @SuppressWarnings("unchecked")
+    public <T> T parse(Class<T> type, String string) {
+        final Function<String, ?> parser = parsers.get(type);
+        if (parser == null) {
+            throw new UnsupportedOperationException(type + " is not supported");
+        }
+        try {
+            return (T) parser.apply(string);
+        } catch (RuntimeException ex) {
+            throw new UncheckedParseException("Failed to parse [" + string + "] as " + type, ex);
+        }
+    }
+
+    /**
+     * Tries to parse the given {@code string} as {@code type} and returns the result.
+     *
+     * @param type   the type class
+     * @param string the string to parse
+     * @param <T>    the type
+     * @return the parsed value for {@code string} as type {@code type},
+     * or {@code Optional.empty()} (if parsing fails, or the type is not {@linkplain #supports supported})
+     */
+    public <T> Optional<?> tryParse(Class<?> type, String string) {
+        try {
+            return Optional.ofNullable(parse(type, string));
+        } catch (RuntimeException ex) {
+            Logging.trace(ex);
+            return Optional.empty();
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/tools/XmlObjectParser.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/XmlObjectParser.java	(revision 16180)
+++ /trunk/src/org/openstreetmap/josm/tools/XmlObjectParser.java	(revision 16181)
@@ -17,4 +17,5 @@
 import java.util.Locale;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Stack;
 
@@ -68,6 +69,8 @@
         private final Stack<Object> current = new Stack<>();
         private StringBuilder characters = new StringBuilder(64);
-
         private Locator locator;
+        private final StringParser primitiveParsers = new StringParser(StringParser.DEFAULT)
+                .registerParser(boolean.class, this::parseBoolean)
+                .registerParser(Boolean.class, this::parseBoolean);
 
         @Override
@@ -123,22 +126,4 @@
         }
 
-        private Object getValueForClass(Class<?> klass, String value) {
-            if (boolean.class.equals(klass))
-                return parseBoolean(value);
-            else if (char.class.equals(klass))
-                return value.charAt(0);
-            else if (short.class.equals(klass) || Short.class.equals(klass))
-                return Integer.valueOf(value);
-            else if (Integer.class.equals(klass))
-                return Integer.valueOf(value);
-            else if (Long.class.equals(klass))
-                return Long.valueOf(value);
-            else if (Float.class.equals(klass))
-                return Float.valueOf(value);
-            else if (Double.class.equals(klass))
-                return Double.valueOf(value);
-            return value;
-        }
-
         private void setValue(Entry entry, String fieldName, String value) throws SAXException {
             if (value != null) {
@@ -157,10 +142,7 @@
                     f = entry.getField("locale_" + fieldName.substring(lang.length()));
                 }
-                if (f != null && Modifier.isPublic(f.getModifiers()) && (
-                        String.class.equals(f.getType()) || boolean.class.equals(f.getType()) || char.class.equals(f.getType()) ||
-                        Float.class.equals(f.getType()) || Double.class.equals(f.getType()) ||
-                        short.class.equals(f.getType()) || Short.class.equals(f.getType()) ||
-                        Long.class.equals(f.getType()) || Integer.class.equals(f.getType()))) {
-                    f.set(c, getValueForClass(f.getType(), value));
+                Optional<?> parsed;
+                if (f != null && Modifier.isPublic(f.getModifiers()) && (parsed = primitiveParsers.tryParse(f.getType(), value)).isPresent()) {
+                    f.set(c, parsed.get());
                 } else {
                     String setter;
@@ -173,5 +155,6 @@
                     Method m = entry.getMethod(setter);
                     if (m != null) {
-                        m.invoke(c, getValueForClass(m.getParameterTypes()[0], value));
+                        parsed = primitiveParsers.tryParse(m.getParameterTypes()[0], value);
+                        m.invoke(c, parsed.isPresent() ? parsed.get() : value);
                     }
                 }
Index: /trunk/test/unit/org/openstreetmap/josm/tools/StringParserTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/tools/StringParserTest.java	(revision 16181)
+++ /trunk/test/unit/org/openstreetmap/josm/tools/StringParserTest.java	(revision 16181)
@@ -0,0 +1,62 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.tools;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Optional;
+
+import net.trajano.commons.testing.UtilityClassTestUtil;
+import org.junit.Test;
+
+/**
+ * Unit tests of {@link StringParser} class.
+ */
+public class StringParserTest {
+
+    /**
+     * Tests that {@code Utils} satisfies utility class criteria.
+     *
+     * @throws ReflectiveOperationException if an error occurs
+     */
+    @Test
+    public void testUtilityClass() throws ReflectiveOperationException {
+        UtilityClassTestUtil.assertUtilityClassWellDefined(Utils.class);
+    }
+
+    /**
+     * Test of {@link StringParser#parse}
+     */
+    @Test
+    public void testParse() {
+        assertThat(StringParser.DEFAULT.parse(char.class, "josm"), is('j'));
+        assertThat(StringParser.DEFAULT.parse(short.class, "123"), is((short) 123));
+        assertThat(StringParser.DEFAULT.parse(int.class, "123456"), is(123456));
+        assertThat(StringParser.DEFAULT.parse(long.class, "1234567890123"), is(1234567890123L));
+        assertThat(StringParser.DEFAULT.tryParse(long.class, "123.456"), is(Optional.empty()));
+        assertThat(StringParser.DEFAULT.tryParse(long.class, "1234567890123"), is(Optional.of(1234567890123L)));
+    }
+
+    /**
+     * Tests that {@link StringParser#DEFAULT} is immutable.
+     */
+    @Test(expected = UnsupportedOperationException.class)
+    public void testDefaultImmutable() {
+        StringParser.DEFAULT.registerParser(String.class, String::valueOf);
+    }
+
+    /**
+     * Tests that {@link StringParser#StringParser(StringParser)} creates a new map.
+     */
+    @Test
+    public void testCopyConstructor() {
+        final StringParser parser = new StringParser(StringParser.DEFAULT).registerParser(boolean.class, "JOSM"::equals);
+        assertTrue(StringParser.DEFAULT.parse(boolean.class, "true"));
+        assertFalse(StringParser.DEFAULT.parse(boolean.class, "JOSM"));
+        assertFalse(parser.parse(boolean.class, "true"));
+        assertTrue(parser.parse(boolean.class, "JOSM"));
+        assertThat(parser.parse(int.class, "123"), is(123));
+    }
+}
