From 240da6c49504a8cfb2b77bcf6bfcf41590a26ff0 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 6 Apr 2026 19:33:21 -0700 Subject: [PATCH] Implement #334: better `Locale` handling for @JsonFormat (via JsonFormat.Value) --- release-notes/VERSION-2.x | 1 + .../jackson/annotation/JsonFormat.java | 32 +++++++++++++++++-- .../jackson/annotation/JsonFormatTest.java | 29 +++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/release-notes/VERSION-2.x b/release-notes/VERSION-2.x index 05147890..d850e3f0 100644 --- a/release-notes/VERSION-2.x +++ b/release-notes/VERSION-2.x @@ -19,6 +19,7 @@ NOTE: Jackson 3.x components rely on 2.x annotations; there are no separate #339: Add `OptBoolean` valued property "order" in `@JsonIncludeProperties` #342: Add `@JsonTypeInfo.writeTypeIdForDefaultImpl` to allow skipping writing of type id for values of default type +#344: Improve `Locale` handling in `JsonFormat.Value` 2.21 (18-Jan-2026) diff --git a/src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java b/src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java index 75ccf5e4..cb40a333 100644 --- a/src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java +++ b/src/main/java/com/fasterxml/jackson/annotation/JsonFormat.java @@ -591,7 +591,7 @@ public Value(String p, Shape sh, String localeStr, String tzStr, Features f, { this(p, sh, (localeStr == null || localeStr.length() == 0 || DEFAULT_LOCALE.equals(localeStr)) ? - null : new Locale(localeStr), + null : _parseLocale(localeStr), (tzStr == null || tzStr.length() == 0 || DEFAULT_TIMEZONE.equals(tzStr)) ? null : tzStr, null, f, lenient, radix); @@ -606,7 +606,7 @@ public Value(String p, Shape sh, String localeStr, String tzStr, Features f, { this(p, sh, (localeStr == null || localeStr.length() == 0 || DEFAULT_LOCALE.equals(localeStr)) ? - null : new Locale(localeStr), + null : _parseLocale(localeStr), (tzStr == null || tzStr.length() == 0 || DEFAULT_TIMEZONE.equals(tzStr)) ? null : tzStr, null, f, lenient); @@ -1047,5 +1047,33 @@ public boolean equals(Object o) { && Objects.equals(_locale, other._locale) && (_radix == other._radix); } + + /** + * Helper method for parsing locale string into {@link Locale}, + * handling both underscore and hyphen as separator (so both + * "en_US" and "en-US" work), as well as optional variant + * (like "en_US_POSIX"). + * + * @since 2.22 + */ + private static Locale _parseLocale(String localeStr) { + final int len = localeStr.length(); + for (int i = 0; i < len; ++i) { + char c = localeStr.charAt(i); + if (c == '_' || c == '-') { + String language = localeStr.substring(0, i); + String rest = localeStr.substring(i + 1); + // Look for second separator for variant + for (int j = 0, rlen = rest.length(); j < rlen; ++j) { + char c2 = rest.charAt(j); + if (c2 == '_' || c2 == '-') { + return new Locale(language, rest.substring(0, j), rest.substring(j + 1)); + } + } + return new Locale(language, rest); + } + } + return new Locale(localeStr); + } } } diff --git a/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java b/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java index ba0766d7..c1998b43 100644 --- a/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java +++ b/src/test/java/com/fasterxml/jackson/annotation/JsonFormatTest.java @@ -311,6 +311,35 @@ void testRadixInHashCode() { assertNotEquals(v1.hashCode(), v2.hashCode()); } + // [annotations#344]: Locale parsing with language, country, variant + @Test + void testLocaleParsingWithCountry() { + // Simple language-only + JsonFormat.Value v = new JsonFormat.Value("", Shape.ANY, "en", "", + JsonFormat.Features.empty(), null, DEFAULT_RADIX); + assertEquals(new java.util.Locale("en"), v.getLocale()); + + // Language + country with underscore + v = new JsonFormat.Value("", Shape.ANY, "en_US", "", + JsonFormat.Features.empty(), null, DEFAULT_RADIX); + assertEquals(new java.util.Locale("en", "US"), v.getLocale()); + + // Language + country with hyphen + v = new JsonFormat.Value("", Shape.ANY, "en-US", "", + JsonFormat.Features.empty(), null, DEFAULT_RADIX); + assertEquals(new java.util.Locale("en", "US"), v.getLocale()); + + // Language + country + variant + v = new JsonFormat.Value("", Shape.ANY, "en_US_POSIX", "", + JsonFormat.Features.empty(), null, DEFAULT_RADIX); + assertEquals(new java.util.Locale("en", "US", "POSIX"), v.getLocale()); + + // German locale + v = new JsonFormat.Value("", Shape.ANY, "de_DE", "", + JsonFormat.Features.empty(), null, DEFAULT_RADIX); + assertEquals(new java.util.Locale("de", "DE"), v.getLocale()); + } + @Test void testRadix() { //Non-Default radix overrides the default