I'm investigating the use of YAML for a somewhat complicated metadata language. It would help to make the documents smaller and less complex if we could use YAML's anchors and references. I've written some test code that seems to show that Jackson's YAML implementation doesn't support this feature (and/or doesn't surface SnakeYAML's support for this feature).
Here is my test YAML file:
set_one:
bass: tama rockstar 22x16
snare: &ludwig ludwig supralight 6.5x15
tom1: tama rockstar 12x11
tom2: tama rockstar 16x16
set_two:
snare: *ludwig
I'm parsing this file like so:
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
FileInputStream fis = null;
try
{
fis = new FileInputStream(file);
JsonNode nodeTree = mapper.readTree(fis);
examineObject(nodeTree, 0);
}
...
Here is the out from my "examineObject()" method (you can probably guess what it does):
key = "set_one", type = OBJECT
key = "bass", type = STRING, value = "tama rockstar 22x16"
key = "snare", type = STRING, value = "ludwig supralight 6.5x15"
key = "tom1", type = STRING, value = "tama rockstar 12x11"
key = "tom2", type = STRING, value = "tama rockstar 16x16"
key = "set_two", type = OBJECT
key = "snare", type = STRING, value = "ludwig"
Clearly something knew enough to omit the anchor value from "set_one.snare" but, in the debugger, I can't find that value anywhere in the JsonNode for this element. The real problem is that the value of "set_two.snare" is just "ludwig". The reference symbol ('*') has been stripped, but the value is that of the reference and not the element it is referring to.
I'm using Jackson version 2.8.3 and SnakeYaml version 1.17. I am constrained to using Jackson as this is only part of a much bigger project which already uses Jackson for JSON.
What I would really like is if Jackson could automatically resolve references and make a copy of the referenced value. In my example this would mean that the value of "set_two.snare" would be "ludwig supralight 6.5x15".
If I can't get my first choice then I would like Jackson to preserve both the anchors and the references so that I could manually post-process the node tree and resolve the references myself. For example, when I saw that the value of "set_two.snare" was "*ludwig", I could search the tree for a node with an anchor of "&ludwig" and make a copy of that node.
If there is an answer, I have the feeling that it will probably involve the "com.fasterxml.jackson.dataformat.yaml.YAMLParser.Feature" class somehow. Unfortunately I can't find any documentation on the features (if they exist) that will enable the behavior I am looking for.
First, Jackson actually does support YAML anchors and references, at least to degree they work with how Jackson supports Object Id references with @JsonIdentityInfo
: limitation being that you can not -- for example -- refer to one key/value pair of am Object.
But identity id/reference handling is only enabled for types and properties specified by annotating then with @JsonIdentityInfo
.
So you have to annotate either types that may be referenced, or properties (no need to do both).
One thing that may help here is to consider that Object Id handling by Jackson is very similar for all formats: so although jackson-dataformat-yaml
does expose "native" Object (and Type) Ids that YAML has (and JSON does not have), handling at databinding level is identical. So if you can make Object Id/References work with JSON (which adds extra id property), it will work with YAML as well.
There is one extra thing that is related: YAMLParser.Feature.USE_NATIVE_OBJECT_ID
which determines how references and ids are expressed when writing YAML -- by default, it uses native anchors, but it can be turned off to use "JSON-like" plain properties.
I hope this helps. For additional help the best place would be jackson-users
mailing list.
Since Jackson YAML does not support Anchors and references (issue), its best to fall-back to SnakeYaml to parse the YAML file & then convert it to Jackson's format to leverage the goodness of Jackson's flexibility. Snake Yaml has support for anchors, unlike Jackson (though Jackson uses snakeyaml in the background to parse Yaml files).
import java.io.{File, FileInputStream, FileReader}
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
import com.fasterxml.jackson.module.scala.DefaultScalaModule
import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper}
import org.yaml.snakeyaml.Yaml
// Parsing the YAML file with SnakeYAML - since Jackson Parser does not support Anchors and references
val ios = new FileInputStream(new File(yamlFilePath))
val yaml = new Yaml()
val mapper = new ObjectMapper().registerModules(DefaultScalaModule)
val yamlObj = yaml.loadAs(ios, classOf[Any])
// Converting the YAML to Jackson YAML - since it has more flexibility
val jsonString = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(yamlObj) // Formats YAML to a pretty printed JSON string - easy to read
val jsonObj = mapper.readTree(jsonString)
The resulting jsonObj is a JsonNode, which is one of the core data formats in Jackson. We can use the as & get methods to easily traverse through the YAML file:
jsonObj.at("/parent/first_level_child/second_level_child")
jsonObj.get("key")
As YAML format is very close to JSON format there should not be data loss in most cases. Moreover, Jackson's JsonNode format allows us to use the additional flexibility from Jackson-Json parsers - which is missing in Jackson-YAML parsers.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With