Sherezad
Sherezad

Reputation: 91

Jackson YAML Serialization Object Arrays Format

I am trying to format my Jackson Yaml serialization in a certain way.

employees:
 - name: John
   age: 26
 - name: Bill
   age: 17

But, when I serialize the object, this is the format that I get.

employees:
 -
  name: John
  age: 26
 -
  name: Bill
  age: 17

Is there any way to get rid of the newline at the start of an object in the array? This is purely a personal preference/human readability issue.

These are the properties I'm currently setting on the YAMLFactory:

YAMLFactory yamlFactory = new YAMLFactory()
                .enable(YAMLGenerator.Feature.MINIMIZE_QUOTES) //removes quotes from strings
                .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)//gets rid of -- at the start of the file.
                .enable(YAMLGenerator.Feature.INDENT_ARRAYS);// enables indentation.

I've looked in the java docs for the YAMLGenerator in Jackson, and looked at the other questions on stackoverflow, but I can't find an option to do what I'm trying to do.

I've tried CANONICAL_OUTPUT, SPLIT_LINES and LITERAL_BLOCK_STYLE properties, the last one being automatically set when MINIMIZE_QUOTES is set. CANONICAL_OUTPUT seems to add brackets around arrays. SPLIT_LINES and LITERAL_BLOCK_STYLE are related to how multi-line strings are handled.

Upvotes: 5

Views: 4353

Answers (4)

Chris
Chris

Reputation: 3657

In today's environment using at least version 2.12.x of jackson-dataformat-yaml thanks to the introduction of YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR, you can simply just do:

public ObjectMapper yamlObjectMapper() {
    final YAMLFactory factory = new YAMLFactory()
            .enable(YAMLGenerator.Feature.INDENT_ARRAYS_WITH_INDICATOR)
            .enable(YAMLGenerator.Feature.MINIMIZE_QUOTES)
            .disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);

    return new ObjectMapper(factory);
}

Which will give you the output of:

employees:
 - name: John
   age: 26
 - name: Bill
   age: 17

Upvotes: 1

Christian Gruber
Christian Gruber

Reputation: 4761

I ran into this and ended up writing this blog entry to describe the solution I came up with. In short, I made a custom subclass of YAMLGenerator and YAMLFactory and used that to configure Jackson's YAMLMapper. Not "clean" but not huge and reasonably effective. Let me set arbitrary DumperOptions

Source below, but also available in this gist.

Warning - I did this all in Kotlin, but it's trivial code, and should be easily back-portable to Java:

val mapper: YAMLMapper = YAMLMapper(MyYAMLFactory()).apply {
  registerModule(KotlinModule())
  setSerializationInclusion(JsonInclude.Include.NON_EMPTY)
}


class MyYAMLGenerator(
  ctx: IOContext,
  jsonFeatures: Int,
  yamlFeatures: Int,
  codec: ObjectCodec,
  out: Writer,
  version: DumperOptions.Version?
): YAMLGenerator(ctx, jsonFeatures, yamlFeatures, codec, out, version) {
  override fun buildDumperOptions(
    jsonFeatures: Int,
    yamlFeatures: Int,
    version: DumperOptions.Version?
  ): DumperOptions {
    return super.buildDumperOptions(jsonFeatures, yamlFeatures, version).apply {
      //
      // NOTE: CONFIGURATION HAPPENS HERE!!
      //
      defaultScalarStyle = ScalarStyle.LITERAL;
      defaultFlowStyle = FlowStyle.BLOCK
      indicatorIndent = 2
      nonPrintableStyle = ESCAPE
      indent = 4
      isPrettyFlow = true
      width = 100
      this.version = version
    }
  }
}

class MyYAMLFactory(): YAMLFactory() {
  @Throws(IOException::class)
  override fun _createGenerator(out: Writer, ctxt: IOContext): YAMLGenerator {
    val feats = _yamlGeneratorFeatures
    return MyYAMLGenerator(ctxt, _generatorFeatures, feats,_objectCodec, out, _version)
  }
}

Upvotes: 0

Marcel Overdijk
Marcel Overdijk

Reputation: 11467

Looking at the Jackson source code the snakeyaml dumper options are created like: @Andrey does this look good to you?

    protected DumperOptions buildDumperOptions(int jsonFeatures, int yamlFeatures,
            org.yaml.snakeyaml.DumperOptions.Version version)
    {
        DumperOptions opt = new DumperOptions();
        // would we want canonical?
        if (Feature.CANONICAL_OUTPUT.enabledIn(_formatFeatures)) {
            opt.setCanonical(true);
        } else {
            opt.setCanonical(false);
            // if not, MUST specify flow styles
            opt.setDefaultFlowStyle(FlowStyle.BLOCK);
        }
        // split-lines for text blocks?
        opt.setSplitLines(Feature.SPLIT_LINES.enabledIn(_formatFeatures));
        // array indentation?
        if (Feature.INDENT_ARRAYS.enabledIn(_formatFeatures)) {
            // But, wrt [dataformats-text#34]: need to set both to diff values to work around bug
            // (otherwise indentation level is "invisible". Note that this should NOT be necessary
            // but is needed up to at least SnakeYAML 1.18.
            // Also looks like all kinds of values do work, except for both being 2... weird.
            opt.setIndicatorIndent(1);
            opt.setIndent(2);
        }
        // 14-May-2018: [dataformats-text#84] allow use of platform linefeed
        if (Feature.USE_PLATFORM_LINE_BREAKS.enabledIn(_formatFeatures)) {
            opt.setLineBreak(DumperOptions.LineBreak.getPlatformLineBreak());
        }
        return opt;
    }

Upvotes: 0

Sherezad
Sherezad

Reputation: 91

The short answer is there is currently no way to do this through Jackson. This is due to a bug within snakeyaml where if you set the indicatorIndent property, the whitespace is not handled properly, and therefore snakeyaml adds the new line.

I have found a workaround using snakeyaml directly.

//The representer allows us to ignore null properties, and to leave off the class definitions
Representer representer = new Representer() {
    //ignore null properties
    @Override
    protected NodeTuple representJavaBeanProperty(Object javaBean, Property property, Object propertyValue, Tag customTag) {
        // if value of property is null, ignore it.
        if (propertyValue == null) {
            return null;
        }
        else {
            return super.representJavaBeanProperty(javaBean, property, propertyValue, customTag);
        }
    }

    //Don't print the class definition
    @Override
    protected MappingNode representJavaBean(Set<Property> properties, Object javaBean) {
        if (!classTags.containsKey(javaBean.getClass())){
            addClassTag(javaBean.getClass(), Tag.MAP);
           }

        return super.representJavaBean(properties, javaBean);
    }
};



DumperOptions dumperOptions = new DumperOptions();
//prints the yaml as nested blocks
dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK);
//indicatorIndent indents the '-' character for lists
dumperOptions.setIndicatorIndent(2);
//This is the workaround. Indent must be set to at least 2 higher than the indicator indent because of how whitespace is handled.
//If not set to 2 higher, then the newline is added.
dumperOptions.setIndent(4);
Yaml yaml = new Yaml(representer, dumperOptions);
//prints the object to a yaml string.
yaml.dump(object);

The workaround happens with setting the indent property on the DumperOptions. You need to set the indent to a value at least 2 higher than the indicatorIndent, or else the newline will be added. This is due to how whitespace is handled within snakeyaml.

Upvotes: 2

Related Questions