Skip to content

Commit 57bdcce

Browse files
committed
Fix #589: support non-scalar YAML mapping keys in YAMLParser
When SnakeYAML emits SequenceStart or MappingStart for an explicit complex key (? [...] or ? {...}), consume the key structure and expose it as a flow-style string property name so YAMLMapper/readTree can parse the document.
1 parent e4d3964 commit 57bdcce

2 files changed

Lines changed: 168 additions & 0 deletions

File tree

yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLParser.java

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,15 @@ public JsonToken nextToken() throws JacksonException
356356
_streamReadContext = _streamReadContext.getParent();
357357
return _updateToken(JsonToken.END_OBJECT);
358358
}
359+
// [dataformats-text#589]: complex (non-scalar) mapping keys
360+
if (evt.getEventId() == Event.ID.SequenceStart
361+
|| evt.getEventId() == Event.ID.MappingStart) {
362+
final String name = _decodeComplexPropertyName(evt);
363+
_streamReadConstraints.validateNameLength(name.length());
364+
_currentName = name;
365+
_streamReadContext.setCurrentName(name);
366+
return _updateToken(JsonToken.PROPERTY_NAME);
367+
}
359368
_reportError("Expected a property name (Scalar value in YAML), got this instead: "+evt);
360369
}
361370

@@ -481,6 +490,119 @@ protected Event nextEvent() {
481490
return _yamlParser.next();
482491
}
483492

493+
/**
494+
* Helper method called when a non-scalar event (Sequence or Mapping start) is encountered
495+
* where a property name is expected -- indicates a "complex key" in YAML terms.
496+
* Consumes the full key structure and converts to a canonical String representation
497+
* (flow-style) so that Jackson's scalar-only property name model can represent it.
498+
*
499+
* @since 3.2
500+
*/
501+
protected String _decodeComplexPropertyName(Event evt) throws JacksonException
502+
{
503+
StringBuilder sb = new StringBuilder(32);
504+
_appendComplexKey(evt, sb, 0);
505+
return sb.toString();
506+
}
507+
508+
private void _appendComplexKey(Event evt, StringBuilder sb, int depth) throws JacksonException
509+
{
510+
_streamReadConstraints.validateNestingDepth(depth);
511+
switch (evt.getEventId()) {
512+
case Scalar:
513+
_appendComplexKeyScalar(((ScalarEvent) evt).getValue(), sb);
514+
return;
515+
case SequenceStart:
516+
sb.append('[');
517+
_appendComplexKeySequenceContent(sb, depth + 1);
518+
sb.append(']');
519+
return;
520+
case MappingStart:
521+
sb.append('{');
522+
_appendComplexKeyMappingContent(sb, depth + 1);
523+
sb.append('}');
524+
return;
525+
default:
526+
_reportError("Unexpected YAML event for complex property name, expected Scalar, Sequence or Mapping start, got: "+evt);
527+
}
528+
}
529+
530+
private void _appendComplexKeySequenceContent(StringBuilder sb, int depth) throws JacksonException
531+
{
532+
boolean first = true;
533+
while (true) {
534+
Event evt = nextEvent();
535+
if (evt == null) {
536+
_reportError("Unexpected end-of-input in complex property name (sequence)");
537+
}
538+
if (evt.getEventId() == Event.ID.SequenceEnd) {
539+
return;
540+
}
541+
if (!first) {
542+
sb.append(", ");
543+
}
544+
first = false;
545+
_appendComplexKey(evt, sb, depth);
546+
}
547+
}
548+
549+
private void _appendComplexKeyMappingContent(StringBuilder sb, int depth) throws JacksonException
550+
{
551+
boolean first = true;
552+
while (true) {
553+
Event keyEvt = nextEvent();
554+
if (keyEvt == null) {
555+
_reportError("Unexpected end-of-input in complex property name (mapping)");
556+
}
557+
if (keyEvt.getEventId() == Event.ID.MappingEnd) {
558+
return;
559+
}
560+
if (!first) {
561+
sb.append(", ");
562+
}
563+
first = false;
564+
_appendComplexKey(keyEvt, sb, depth);
565+
sb.append(": ");
566+
Event valEvt = nextEvent();
567+
if (valEvt == null) {
568+
_reportError("Unexpected end-of-input in complex property name (mapping value)");
569+
}
570+
_appendComplexKey(valEvt, sb, depth);
571+
}
572+
}
573+
574+
private void _appendComplexKeyScalar(String value, StringBuilder sb)
575+
{
576+
if (_needsQuotesForComplexKey(value)) {
577+
sb.append('"');
578+
for (int i = 0, len = value.length(); i < len; ++i) {
579+
char c = value.charAt(i);
580+
if (c == '"' || c == '\\') {
581+
sb.append('\\');
582+
}
583+
sb.append(c);
584+
}
585+
sb.append('"');
586+
} else {
587+
sb.append(value);
588+
}
589+
}
590+
591+
private boolean _needsQuotesForComplexKey(String value)
592+
{
593+
if (value.isEmpty()) {
594+
return true;
595+
}
596+
for (int i = 0, len = value.length(); i < len; ++i) {
597+
char c = value.charAt(i);
598+
if (c == ':' || c == ',' || c == '[' || c == ']' || c == '{' || c == '}'
599+
|| c == ' ' || c == '\t' || c == '\n' || c == '\r') {
600+
return true;
601+
}
602+
}
603+
return false;
604+
}
605+
484606
protected JsonToken _decodeScalar(ScalarEvent scalar) throws JacksonException
485607
{
486608
String value = scalar.getValue();

yaml/src/test/java/tools/jackson/dataformat/yaml/deser/StreamingYAMLParseTest.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import tools.jackson.core.JsonParser;
1212
import tools.jackson.core.JsonToken;
13+
import tools.jackson.databind.JsonNode;
1314
import tools.jackson.dataformat.yaml.JacksonYAMLParseException;
1415
import tools.jackson.dataformat.yaml.ModuleTestBase;
1516
import tools.jackson.dataformat.yaml.YAMLFactory;
@@ -663,6 +664,51 @@ public void testYamlParseFailsWhenCodePointLimitVerySmall() throws Exception
663664
}
664665
}
665666

667+
// [dataformats-text#589]: sequence (and mapping) as mapping key
668+
@Test
669+
public void testComplexMappingKeySequence() throws Exception
670+
{
671+
final String YAML = "? [user, 123]\n: name: Ivan\n age: 30\n";
672+
try (JsonParser p = MAPPER.createParser(YAML)) {
673+
assertToken(JsonToken.START_OBJECT, p.nextToken());
674+
assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
675+
assertEquals("[user, 123]", p.currentName());
676+
assertEquals("[user, 123]", p.getString());
677+
assertToken(JsonToken.START_OBJECT, p.nextToken());
678+
assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
679+
assertEquals("name", p.currentName());
680+
assertToken(JsonToken.VALUE_STRING, p.nextToken());
681+
assertEquals("Ivan", p.getString());
682+
assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
683+
assertEquals("age", p.currentName());
684+
assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
685+
assertEquals(30, p.getIntValue());
686+
assertToken(JsonToken.END_OBJECT, p.nextToken());
687+
assertToken(JsonToken.END_OBJECT, p.nextToken());
688+
assertNull(p.nextToken());
689+
}
690+
691+
JsonNode node = MAPPER.readTree(YAML);
692+
assertTrue(node.has("[user, 123]"));
693+
assertEquals("Ivan", node.get("[user, 123]").get("name").asString());
694+
assertEquals(30, node.get("[user, 123]").get("age").asInt());
695+
}
696+
697+
@Test
698+
public void testComplexMappingKeyMapping() throws Exception
699+
{
700+
final String YAML = "? {foo: bar}\n: 42\n";
701+
try (JsonParser p = MAPPER.createParser(YAML)) {
702+
assertToken(JsonToken.START_OBJECT, p.nextToken());
703+
assertToken(JsonToken.PROPERTY_NAME, p.nextToken());
704+
assertEquals("{foo: bar}", p.currentName());
705+
assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken());
706+
assertEquals(42, p.getIntValue());
707+
assertToken(JsonToken.END_OBJECT, p.nextToken());
708+
assertNull(p.nextToken());
709+
}
710+
}
711+
666712
// In Jackson 3.x, non-Number token should NOT throw exception for parser.getNumberType()
667713
// (like in 2.x) but just return `null`
668714
private void _verifyGetNumberTypeFail(JsonParser p, String token) throws Exception

0 commit comments

Comments
 (0)