Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,15 @@ Sergio Delgado (@serandel)
* Reported #154: (yaml) YAML file with no content throws `MismatchedInputException`
when binding to Object type (POJO etc)
(2.21.0)

Pétrus Pradella (@EverNife)

* Reported #701: (yaml) `ALWAYS_QUOTE_NUMBERS_AS_STRINGS` does not quote YAML 1.1
exponent (`1e5`), hex (`0x1F`) and underscore (`12_34`) number forms
(2.21.6)

seonwoojung (@seonwooj0810)

* Contributed fix for #701: (yaml) `ALWAYS_QUOTE_NUMBERS_AS_STRINGS` does not quote
YAML 1.1 exponent (`1e5`), hex (`0x1F`) and underscore (`12_34`) number forms
(2.21.6)
4 changes: 4 additions & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ Active Maintainers:

2.21.6 (not yet released)

#701: (yaml) `ALWAYS_QUOTE_NUMBERS_AS_STRINGS` does not quote YAML 1.1 exponent
(`1e5`), hex (`0x1F`) and underscore (`12_34`) number forms
(reported by @EverNife)
(fix contributed by @seonwooj0810)
#702: (toml) Expand nesting depth checks for dotted keys
(contributed by @yawkat)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.yaml.snakeyaml.emitter.Emitter;
import org.yaml.snakeyaml.events.*;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.resolver.Resolver;

import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.core.base.GeneratorBase;
Expand Down Expand Up @@ -673,7 +674,7 @@ public void writeString(String text) throws IOException, JsonGenerationException
// If one of reserved values ("true", "null"), or, number, preserve quoting:
} else if (_quotingChecker.needToQuoteValue(text)
|| (Feature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS.enabledIn(_formatFeatures)
&& PLAIN_NUMBER_P.matcher(text).matches())
&& _looksLikeYAMLNumber(text))
) {
style = STYLE_QUOTED;
} else {
Expand All @@ -696,6 +697,52 @@ public void writeString(char[] text, int offset, int len) throws IOException
writeString(new String(text, offset, len));
}

/**
* Checks whether given String value would be re-read as a YAML number if emitted
* unquoted; used by {@link Feature#ALWAYS_QUOTE_NUMBERS_AS_STRINGS}. Combines the
* historical {@link #PLAIN_NUMBER_P} check (retained for backwards compatibility)
* with SnakeYAML's own implicit resolver patterns for {@code int} and {@code float}
* ({@link Resolver#INT}, {@link Resolver#FLOAT}) -- the very patterns the parser uses
* to resolve plain scalars back to numbers -- so that YAML 1.1 exponent
* ({@code 1e5}), hex ({@code 0x1F}) and underscore ({@code 12_34}) forms, which
* {@link #PLAIN_NUMBER_P} does not match, are quoted too and thus round-trip.
* See [dataformats-text#701].
*<p>
* 60-base ("sexagesimal") forms like {@code 1:30} are excluded, to match
* {@link com.fasterxml.jackson.dataformat.yaml.YAMLParser}, which does not decode
* them either.
*
* @since 2.21.6
*/
protected boolean _looksLikeYAMLNumber(String text) {
// 23-Jul-2026, tatu: Regexps are relatively costly so avoid them for the
// common case of "regular" text: all forms matched below have to start
// with one of following characters
if (text.isEmpty()) {
return false;
}
switch (text.charAt(0)) {
case '+': case '-': case '.':
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
break;
default:
return false;
}
// 23-Jul-2026, tatu: `Resolver.INT`/`Resolver.FLOAT` also match 60-base
// ("sexagesimal") forms like "1:30" or "12:00:01". But `YAMLParser` on
// purpose does NOT decode those (see `_decodeNumberScalar()`), since they
// are much more likely to be Times or IP numbers -- so quoting them here
// would only add noise. Colon cannot occur in any other alternative of
// either pattern, so this excludes exactly the 60-base ones.
if (text.indexOf(':') >= 0) {
return false;
}
return PLAIN_NUMBER_P.matcher(text).matches()
|| Resolver.INT.matcher(text).matches()
|| Resolver.FLOAT.matcher(text).matches();
}

@Override
public final void writeString(SerializableString sstr)
throws IOException
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,43 @@ public void testQuoteNumberStoredAsString() throws Exception
"key: \"+125\"", yaml);
}

// [dataformats-text#701]: PLAIN_NUMBER_P missed YAML 1.1 exponent/hex/underscore
// number forms, so ALWAYS_QUOTE_NUMBERS_AS_STRINGS left them unquoted and they were
// re-read as numbers (silent corruption). Verify they are now quoted and round-trip.
@Test
public void testQuoteYAML11NumberFormsStoredAsString701() throws Exception
{
YAMLFactory f = new YAMLFactory();
f.configure(YAMLGenerator.Feature.MINIMIZE_QUOTES, true);
f.configure(YAMLGenerator.Feature.ALWAYS_QUOTE_NUMBERS_AS_STRINGS, true);
YAMLMapper mapper = new YAMLMapper(f);

// forms NOT matched by the old PLAIN_NUMBER_P regex:
for (String value : new String[] { "1e5", "0x1F", "12_34", "1.5e-3", "0b101" }) {
String yaml = mapper.writeValueAsString(Collections.singletonMap("key", value)).trim();
assertEquals("---\nkey: \"" + value + "\"", yaml,
"String '" + value + "' should be quoted");
// ... and survive a read+write cycle through the untyped tree
assertEquals(value, mapper.readTree(yaml).get("key").asText(),
"String '" + value + "' should round-trip");
}

// genuine multi-dot version String stays unquoted (not a YAML number)
String yaml = mapper.writeValueAsString(Collections.singletonMap("key", "2.0.1.2.3")).trim();
assertEquals("---\nkey: 2.0.1.2.3", yaml);

// ... and so do 60-base ("sexagesimal") forms: SnakeYAML's resolver patterns
// match these, but `YAMLParser` does not decode them (Times, IP numbers), so
// quoting them would only add noise
for (String value : new String[] { "1:30", "12:00:01", "3:1" }) {
yaml = mapper.writeValueAsString(Collections.singletonMap("key", value)).trim();
assertEquals("---\nkey: " + value, yaml,
"String '" + value + "' should NOT be quoted");
assertEquals(value, mapper.readTree(yaml).get("key").asText(),
"String '" + value + "' should round-trip");
}
}

@Test
public void testNonQuoteNumberStoredAsString() throws Exception
{
Expand Down