From 3bea110d62c2a17c50f807f4222549275bbddab1 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 6 Apr 2026 18:33:45 -0700 Subject: [PATCH 1/3] Implement #214: YAMLParser.getRawTag() --- release-notes/CREDITS | 4 + release-notes/VERSION | 3 + .../jackson/dataformat/yaml/YAMLParser.java | 28 +++- .../dataformat/yaml/type/RawTagTest.java | 138 ++++++++++++++++++ 4 files changed, 172 insertions(+), 1 deletion(-) create mode 100644 yaml/src/test/java/tools/jackson/dataformat/yaml/type/RawTagTest.java diff --git a/release-notes/CREDITS b/release-notes/CREDITS index 5c4537da..c93c081b 100644 --- a/release-notes/CREDITS +++ b/release-notes/CREDITS @@ -54,6 +54,10 @@ Martin Winandy (@pmwmedia) * Reported #109: (yaml) `YAMLMapper` doesn't close output file if an error occurs (3.2.0) +Alexander Tsvetkov (@nictas) + * Requested #214: (yaml) Expose custom tags via `YAMLParser.getRawTag()` + (3.2.0) + David Xia (@davidxia) * Requested #276: (yaml) Support opting out of octal number interpretation (like "0444"); add `YAMLReadFeature.PARSE_OCTAL_NUMBERS` diff --git a/release-notes/VERSION b/release-notes/VERSION index 949c9a3f..3240d611 100644 --- a/release-notes/VERSION +++ b/release-notes/VERSION @@ -30,6 +30,9 @@ implementations) #109: (yaml) `YAMLMapper` doesn't close output file if an error occurs (reported by Martin W) (fix by @cowtowncoder, w/ Claude code) +#214: (yaml) Expose custom tags via `YAMLParser.getRawTag()` + (requested by Alexander T) + (fix by @cowtowncoder, w/ Claude code) #264: (csv) `CsvMapper` applies alphabetic Property Order for Java Records (reported by @alexpartsch) (fix by @cowtowncoder) diff --git a/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLParser.java b/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLParser.java index 70261884..d1cc801b 100644 --- a/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLParser.java +++ b/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLParser.java @@ -1122,7 +1122,33 @@ public String getTypeId() throws JacksonException } return null; } - + + /** + * Method that can be used to access the YAML tag of the current token, + * in its raw form: that is, without stripping leading "!" prefix(es) + * (unlike {@link #getTypeId()} which does strip them). + *

+ * NOTE: method name is somewhat of a misnomer since YAML actually allows + * at most one tag per node; but since this is a raw accessor for the + * underlying SnakeYAML event tag, we keep the name as-is. + * + * @return Raw YAML tag of the current token, if any; {@code null} if none + * + * @since 3.2 + */ + public String getRawTag() + { + Optional tagOpt; + if (_lastTagEvent instanceof CollectionStartEvent) { + tagOpt = ((CollectionStartEvent) _lastTagEvent).getTag(); + } else if (_lastTagEvent instanceof ScalarEvent) { + tagOpt = ((ScalarEvent) _lastTagEvent).getTag(); + } else { + return null; + } + return tagOpt.orElse(null); + } + /* /********************************************************************** /* Internal methods diff --git a/yaml/src/test/java/tools/jackson/dataformat/yaml/type/RawTagTest.java b/yaml/src/test/java/tools/jackson/dataformat/yaml/type/RawTagTest.java new file mode 100644 index 00000000..b3851069 --- /dev/null +++ b/yaml/src/test/java/tools/jackson/dataformat/yaml/type/RawTagTest.java @@ -0,0 +1,138 @@ +package tools.jackson.dataformat.yaml.type; + +import org.junit.jupiter.api.Test; + +import tools.jackson.core.JsonToken; +import tools.jackson.dataformat.yaml.ModuleTestBase; +import tools.jackson.dataformat.yaml.YAMLFactory; +import tools.jackson.dataformat.yaml.YAMLParser; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for {@link YAMLParser#getRawTag()}. + * + * @since 3.2 + */ +public class RawTagTest extends ModuleTestBase +{ + private final YAMLFactory YAML_F = new YAMLFactory(); + + @Test + public void testCustomScalarTag() throws Exception + { + final String YAML = "---\npassword: !sensitive Abcd1234\n"; + try (YAMLParser p = (YAMLParser) YAML_F.createParser(YAML)) { + assertToken(JsonToken.START_OBJECT, p.nextToken()); + assertNull(p.getRawTag()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("password", p.currentName()); + + assertToken(JsonToken.VALUE_STRING, p.nextToken()); + assertEquals("Abcd1234", p.getText()); + assertEquals("!sensitive", p.getRawTag()); + // getTypeId() should strip the "!" prefix + assertEquals("sensitive", p.getTypeId()); + + assertToken(JsonToken.END_OBJECT, p.nextToken()); + assertNull(p.getRawTag()); + } + } + + @Test + public void testVerbatimTag() throws Exception + { + // Verbatim tag !<...> is resolved by SnakeYAML Engine into the URI content + final String YAML = "--- !\na: 13\n"; + try (YAMLParser p = (YAMLParser) YAML_F.createParser(YAML)) { + assertToken(JsonToken.START_OBJECT, p.nextToken()); + assertEquals("impl", p.getRawTag()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals(13, p.getIntValue()); + + assertToken(JsonToken.END_OBJECT, p.nextToken()); + } + } + + @Test + public void testLocalTag() throws Exception + { + // Local tag !impl keeps the "!" prefix + final String YAML = "--- !impl\na: 13\n"; + try (YAMLParser p = (YAMLParser) YAML_F.createParser(YAML)) { + assertToken(JsonToken.START_OBJECT, p.nextToken()); + assertEquals("!impl", p.getRawTag()); + // getTypeId() strips the "!" prefix + assertEquals("impl", p.getTypeId()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertToken(JsonToken.VALUE_NUMBER_INT, p.nextToken()); + assertEquals(13, p.getIntValue()); + + assertToken(JsonToken.END_OBJECT, p.nextToken()); + } + } + + @Test + public void testNoTag() throws Exception + { + final String YAML = "---\nkey: value\n"; + try (YAMLParser p = (YAMLParser) YAML_F.createParser(YAML)) { + assertToken(JsonToken.START_OBJECT, p.nextToken()); + assertNull(p.getRawTag()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertToken(JsonToken.VALUE_STRING, p.nextToken()); + assertEquals("value", p.getText()); + assertNull(p.getRawTag()); + + assertToken(JsonToken.END_OBJECT, p.nextToken()); + } + } + + @Test + public void testSequenceTag() throws Exception + { + final String YAML = "--- !mylist\n- a\n- b\n"; + try (YAMLParser p = (YAMLParser) YAML_F.createParser(YAML)) { + assertToken(JsonToken.START_ARRAY, p.nextToken()); + assertEquals("!mylist", p.getRawTag()); + + assertToken(JsonToken.VALUE_STRING, p.nextToken()); + assertEquals("a", p.getText()); + assertNull(p.getRawTag()); + + assertToken(JsonToken.VALUE_STRING, p.nextToken()); + assertEquals("b", p.getText()); + assertNull(p.getRawTag()); + + assertToken(JsonToken.END_ARRAY, p.nextToken()); + } + } + + @Test + public void testMultipleCustomTags() throws Exception + { + final String YAML = "---\nuser: !public someone\npass: !sensitive Abcd1234\n"; + try (YAMLParser p = (YAMLParser) YAML_F.createParser(YAML)) { + assertToken(JsonToken.START_OBJECT, p.nextToken()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("user", p.currentName()); + assertToken(JsonToken.VALUE_STRING, p.nextToken()); + assertEquals("someone", p.getText()); + assertEquals("!public", p.getRawTag()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertEquals("pass", p.currentName()); + assertToken(JsonToken.VALUE_STRING, p.nextToken()); + assertEquals("Abcd1234", p.getText()); + assertEquals("!sensitive", p.getRawTag()); + + assertToken(JsonToken.END_OBJECT, p.nextToken()); + } + } +} From d5dd131b67d79cbff0d543c154c6b4c70c7872e1 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 6 Apr 2026 18:37:51 -0700 Subject: [PATCH 2/3] Cleaning up --- .../jackson/dataformat/yaml/YAMLParser.java | 43 ++++++++----------- .../dataformat/yaml/type/RawTagTest.java | 38 ++++++++++++---- 2 files changed, 47 insertions(+), 34 deletions(-) diff --git a/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLParser.java b/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLParser.java index d1cc801b..3dac6e64 100644 --- a/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLParser.java +++ b/yaml/src/main/java/tools/jackson/dataformat/yaml/YAMLParser.java @@ -1100,27 +1100,15 @@ public String getObjectId() throws JacksonException @Override public String getTypeId() throws JacksonException { - Optional tagOpt; - if (_lastTagEvent instanceof CollectionStartEvent) { - tagOpt = ((CollectionStartEvent) _lastTagEvent).getTag(); - //System.err.println("getTypeId() at "+currentToken()+", last was collection ("+_lastTagEvent.getClass().getSimpleName()+") -> "+tag); - } else if (_lastTagEvent instanceof ScalarEvent) { - tagOpt = ((ScalarEvent) _lastTagEvent).getTag(); - //System.err.println("getTypeId() at "+currentToken()+", last was scalar -> "+tag+", scalar == "+_lastEvent); - } else { -//System.err.println("getTypeId(), something else, curr token: "+currentToken()); - return null; - } - if (tagOpt.isPresent()) { - String tag = tagOpt.get(); + String tag = _currentTag().orElse(null); + if (tag != null) { // 04-Aug-2013, tatu: Looks like YAML parser's expose these in... somewhat exotic // ways sometimes. So let's prepare to peel off some wrappings: while (tag.startsWith("!")) { tag = tag.substring(1); } - return tag; } - return null; + return tag; } /** @@ -1128,9 +1116,9 @@ public String getTypeId() throws JacksonException * in its raw form: that is, without stripping leading "!" prefix(es) * (unlike {@link #getTypeId()} which does strip them). *

- * NOTE: method name is somewhat of a misnomer since YAML actually allows - * at most one tag per node; but since this is a raw accessor for the - * underlying SnakeYAML event tag, we keep the name as-is. + * Note that "raw" here means raw with respect to Jackson's processing; + * SnakeYAML Engine may have already resolved some tag syntax (for example, + * verbatim tags like {@code !} are resolved to their URI content). * * @return Raw YAML tag of the current token, if any; {@code null} if none * @@ -1138,15 +1126,18 @@ public String getTypeId() throws JacksonException */ public String getRawTag() { - Optional tagOpt; - if (_lastTagEvent instanceof CollectionStartEvent) { - tagOpt = ((CollectionStartEvent) _lastTagEvent).getTag(); - } else if (_lastTagEvent instanceof ScalarEvent) { - tagOpt = ((ScalarEvent) _lastTagEvent).getTag(); - } else { - return null; + return _currentTag().orElse(null); + } + + // @since 3.2 + protected Optional _currentTag() + { + if (_lastTagEvent instanceof CollectionStartEvent cse) { + return cse.getTag(); + } else if (_lastTagEvent instanceof ScalarEvent se) { + return se.getTag(); } - return tagOpt.orElse(null); + return Optional.empty(); } /* diff --git a/yaml/src/test/java/tools/jackson/dataformat/yaml/type/RawTagTest.java b/yaml/src/test/java/tools/jackson/dataformat/yaml/type/RawTagTest.java index b3851069..ad58b800 100644 --- a/yaml/src/test/java/tools/jackson/dataformat/yaml/type/RawTagTest.java +++ b/yaml/src/test/java/tools/jackson/dataformat/yaml/type/RawTagTest.java @@ -2,9 +2,10 @@ import org.junit.jupiter.api.Test; +import tools.jackson.core.JsonParser; import tools.jackson.core.JsonToken; import tools.jackson.dataformat.yaml.ModuleTestBase; -import tools.jackson.dataformat.yaml.YAMLFactory; +import tools.jackson.dataformat.yaml.YAMLMapper; import tools.jackson.dataformat.yaml.YAMLParser; import static org.junit.jupiter.api.Assertions.*; @@ -16,13 +17,13 @@ */ public class RawTagTest extends ModuleTestBase { - private final YAMLFactory YAML_F = new YAMLFactory(); + private final YAMLMapper MAPPER = new YAMLMapper(); @Test public void testCustomScalarTag() throws Exception { final String YAML = "---\npassword: !sensitive Abcd1234\n"; - try (YAMLParser p = (YAMLParser) YAML_F.createParser(YAML)) { + try (YAMLParser p = (YAMLParser) MAPPER.createParser(YAML)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); assertNull(p.getRawTag()); @@ -45,7 +46,7 @@ public void testVerbatimTag() throws Exception { // Verbatim tag !<...> is resolved by SnakeYAML Engine into the URI content final String YAML = "--- !\na: 13\n"; - try (YAMLParser p = (YAMLParser) YAML_F.createParser(YAML)) { + try (YAMLParser p = (YAMLParser) MAPPER.createParser(YAML)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); assertEquals("impl", p.getRawTag()); @@ -62,7 +63,7 @@ public void testLocalTag() throws Exception { // Local tag !impl keeps the "!" prefix final String YAML = "--- !impl\na: 13\n"; - try (YAMLParser p = (YAMLParser) YAML_F.createParser(YAML)) { + try (YAMLParser p = (YAMLParser) MAPPER.createParser(YAML)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); assertEquals("!impl", p.getRawTag()); // getTypeId() strips the "!" prefix @@ -80,7 +81,7 @@ public void testLocalTag() throws Exception public void testNoTag() throws Exception { final String YAML = "---\nkey: value\n"; - try (YAMLParser p = (YAMLParser) YAML_F.createParser(YAML)) { + try (YAMLParser p = (YAMLParser) MAPPER.createParser(YAML)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); assertNull(p.getRawTag()); @@ -97,7 +98,7 @@ public void testNoTag() throws Exception public void testSequenceTag() throws Exception { final String YAML = "--- !mylist\n- a\n- b\n"; - try (YAMLParser p = (YAMLParser) YAML_F.createParser(YAML)) { + try (YAMLParser p = (YAMLParser) MAPPER.createParser(YAML)) { assertToken(JsonToken.START_ARRAY, p.nextToken()); assertEquals("!mylist", p.getRawTag()); @@ -113,11 +114,32 @@ public void testSequenceTag() throws Exception } } + @Test + public void testSecondaryTagHandle() throws Exception + { + // "!!" is the secondary tag handle, resolved by SnakeYAML Engine + // to the "tag:yaml.org,2002:" prefix + final String YAML = "---\nvalue: !!str 123\n"; + try (YAMLParser p = (YAMLParser) MAPPER.createParser(YAML)) { + assertToken(JsonToken.START_OBJECT, p.nextToken()); + + assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); + assertToken(JsonToken.VALUE_STRING, p.nextToken()); + assertEquals("123", p.getText()); + // Raw tag includes the resolved "tag:yaml.org,2002:" prefix + assertEquals("tag:yaml.org,2002:str", p.getRawTag()); + // getTypeId() strips "!" but this tag has none, so same value + assertEquals("tag:yaml.org,2002:str", p.getTypeId()); + + assertToken(JsonToken.END_OBJECT, p.nextToken()); + } + } + @Test public void testMultipleCustomTags() throws Exception { final String YAML = "---\nuser: !public someone\npass: !sensitive Abcd1234\n"; - try (YAMLParser p = (YAMLParser) YAML_F.createParser(YAML)) { + try (YAMLParser p = (YAMLParser) MAPPER.createParser(YAML)) { assertToken(JsonToken.START_OBJECT, p.nextToken()); assertToken(JsonToken.PROPERTY_NAME, p.nextToken()); From 9f195fe929741276abfdd0472a4ac990bce4b446 Mon Sep 17 00:00:00 2001 From: Tatu Saloranta Date: Mon, 6 Apr 2026 18:39:31 -0700 Subject: [PATCH 3/3] ... --- .../test/java/tools/jackson/dataformat/yaml/type/RawTagTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/yaml/src/test/java/tools/jackson/dataformat/yaml/type/RawTagTest.java b/yaml/src/test/java/tools/jackson/dataformat/yaml/type/RawTagTest.java index ad58b800..35138bbd 100644 --- a/yaml/src/test/java/tools/jackson/dataformat/yaml/type/RawTagTest.java +++ b/yaml/src/test/java/tools/jackson/dataformat/yaml/type/RawTagTest.java @@ -2,7 +2,6 @@ import org.junit.jupiter.api.Test; -import tools.jackson.core.JsonParser; import tools.jackson.core.JsonToken; import tools.jackson.dataformat.yaml.ModuleTestBase; import tools.jackson.dataformat.yaml.YAMLMapper;