Search code examples
javajacksonyamlfasterxmlsnakeyaml

Dumping YAML to String


I'm trying to dump a YAML object to String but I'm facing some problems. This is the original YAML info:

trace_enabled: false
error_gen_enabled: false
trace_info:
  filter: "filter-12345"
  status: 200
  method: "PUT"
error_gen_info:
  code: 500
  rate: 0.35
  content_type: 'application/problem+json'
  content: '{"status":503,"title":"Internal Server Error","detail":"Too busy","cause":""}'

If I try to dump with fasterxml.jackson I try the following:

ObjectMapper mapper = new ObjectMapper(new YAMLFactory().disable(Feature.WRITE_DOC_START_MARKER));
mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
try {
    result = mapper.writeValueAsString(yamlObject);
} catch (JsonProcessingException e) {
    log.warn("Error parsing YAML object to string. Message: {}. Cause: {}", e.getMessage(), e.getCause());
}

And results:

trace_enabled: false
error_gen_enabled: false
trace_info:
  filter: "filter-12345"
  status: 200
  method: "PUT"
error_gen_info:
  code: 500
  rate: 0.35
  content_type: "application/problem+json"
  content: "{\"status\":503,\"title\":\"Internal Server Error\",\"detail\":\"Too busy\"\
    ,\"cause\":\"\"}"

So it can be seen in the original that "trace_info" String values are double quoted, and "error_gen_info" are single quoted. I'd like that "content_type" and "content" would be single quoted, as now is double quoted and the value json looks terrible.

Now if I try with snakeyaml I do:

Representer representer = new Representer();
representer.addClassTag(ErrorGenInfo.class, Tag.OMAP);
representer.setDefaultFlowStyle(FlowStyle.BLOCK);

TypeDescription errorGenInfoDesc = new TypeDescription(ErrorGenInfo.class);
errorGenInfoDesc.substituteProperty("content_type", String.class, "getContentType", "setContentType");
representer.addTypeDescription(errorGenInfoDesc);
errorGenInfoDesc.setExcludes("contentType");

Yaml yaml = new Yaml(representer);
result = yaml.dump(yamlObject);

And I get:

!!com.test.BasicTest$Config
errorGenEnabled: false
errorGenInfo:
  code: 500
  content: '{"status":503,"title":"Internal Server Error","detail":"Too busy","cause":""}'
  contentType: application/problem+json
  rate: 0.35
traceEnabled: false
traceInfo:
  filter: filter-12345
  method: PUT
  status: 200

Here I get a different result:

  1. Why do I get the first line with the class? Is there any way to avoid it?
  2. It does not keep the order, but this is not so important.
  3. The "content" is processed as I want, but none of the String values are quoted. Is there any way to say that string values from "traceInfo" have to be double quoted and string values from "errorGenInfo" have to be single quoted?
  4. Important: the key names are are not separated by underscore, despite I added some code that I saw in other posts (testing TypeDescription.substituteProperty(...) with "content-type"). It seems is not working.

Having done this, how could I get the same format in the output with any of these 2 ways of processing YAML? Thanks in advance.


Solution

  • From the source code of Jackson's YAMLGenerator, it didn't look like it was possible to single quote strings at all. The closest I could get to your expected output is using SnakeYAML directly. No String values were quoted except for content_type field which was single quoted as expected. And the order of fields with the POJOs wasn't maintained. I did manage to remove the tag with the class name and also to rename the field names to be snake case.

    Code:

    import io.github.devatherock.domain.ErrorGenInfo;
    import io.github.devatherock.domain.ResponseBody;
    import io.github.devatherock.domain.TraceInfo;
    import lombok.extern.slf4j.Slf4j;
    import org.yaml.snakeyaml.DumperOptions;
    import org.yaml.snakeyaml.DumperOptions.FlowStyle;
    import org.yaml.snakeyaml.TypeDescription;
    import org.yaml.snakeyaml.Yaml;
    import org.yaml.snakeyaml.nodes.Tag;
    import org.yaml.snakeyaml.representer.Representer;
    
    import java.util.Arrays;
    import java.util.Collections;
    import java.util.List;
    
    @Slf4j
    public class TestUtil {
        public static String toYaml(ResponseBody yamlObject) {
            DumperOptions dumperOptions = new DumperOptions();
            dumperOptions.setDefaultFlowStyle(FlowStyle.BLOCK);
            dumperOptions.setSplitLines(false);
    
            Representer representer = new Representer(dumperOptions);
    
            // To rename fields
            addTypeDescription(
                    Collections.singletonList("content_type"), Collections.singletonList("getContentType"),
                    Collections.singletonList("setContentType"), new String[]{"contentType"},
                    Collections.singletonList(String.class), ErrorGenInfo.class, representer
            );
            addTypeDescription(
                    Arrays.asList("trace_enabled", "error_gen_enabled", "trace_info", "error_gen_info"),
                    Arrays.asList("isTraceEnabled", "isErrorGenEnabled", "getTraceInfo", "getErrorGenInfo"),
                    Arrays.asList("setTraceEnabled", "setErrorGenEnabled", "setTraceInfo", "setErrorGenInfo"),
                    new String[]{"traceEnabled", "errorGenEnabled", "traceInfo", "errorGenInfo"},
                    Arrays.asList(Boolean.class, Boolean.class, TraceInfo.class, ErrorGenInfo.class),
                    ResponseBody.class, representer
            );
    
            // To disable the tags with class name
            representer.addClassTag(ResponseBody.class, Tag.MAP);
            representer.addClassTag(ErrorGenInfo.class, Tag.MAP);
    
            Yaml yaml = new Yaml(representer);
            String result = yaml.dump(yamlObject);
    
            LOGGER.info("Result: \n{}", result);
            return result;
        }
    
        public static void addTypeDescription(
                List<String> propertyNames, List<String> getterNames, List<String> setterNames,
                String[] propertiesToExclude, List<Class<?>> propertyClasses,
                Class<?> beanClass, Representer representer
        ) {
            TypeDescription typeDescription = new TypeDescription(beanClass);
    
            for (int index = 0; index < propertyNames.size(); index++) {
                typeDescription.substituteProperty(
                        propertyNames.get(index), propertyClasses.get(index), getterNames.get(index), setterNames.get(index)
                );
            }
            typeDescription.setExcludes(propertiesToExclude);
    
            representer.addTypeDescription(typeDescription);
        }
    }
    

    Output YAML:

    trace_enabled: false
    error_gen_enabled: false
    trace_info:
      filter: filter-12345
      method: PUT
      status: 200
    error_gen_info:
      content_type: application/problem+json
      code: 500
      content: '{"status":503,"title":"Internal Server Error","detail":"Too busy","cause":""}'
      rate: 0.35