Skip to Content

Usually REST APIs produce minimized JSON without any line breaks or indentation, which preserves bandwidth and is fine if a machine is reading the produced JSON.

If however the intended audience mainly consists of humans, e.g. if the JSON files need to undergo “code” reviews, it is better to pretty-print the JSON files first and not rely on everyone having a JSON-aware editor at hand.

In my case the JSON was produced by an ABAP server, and Google pointed me to Horst Keller’s blog on ABAP and JSON.

My first idea was to call transformation id_indent on my JSON string, but that didn’t work. So I set out to write my own JSON-pretty-printing XSL transformation, and that proved to be pretty easy, thanks to the transformation of JSON into JSON-XML built into call transformation:

<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:sap="http://www.sap.com/sapxsl" version="1.0">
  <!--

    Pretty-print JSON with ABAP XSLT 
    See http://help.sap.com/abapdocu_740/en/index.htm?file=abenabap_json_trafos.htm for the internal JSON-XML format

    data(lv_in) = `{"Hello":"World","I'm":51,"That's":true,"nothing":null,"one":{},"many":[null,false,42,"hi",{"a":"b"},[]]}`.
    data lv_out type string.
    call transformation zrha_pretty_json source xml lv_in result xml lv_out.

  -->

  <xsl:output encoding="UTF-8" indent="yes" method="text" omit-xml-declaration="yes" />
  <xsl:strip-space elements="*" />

  <xsl:template match="object">
    <xsl:param name="indent" select="''" />
    <xsl:if test="position() &gt; 1">
      <value-of select="',& #xA;'"/>
    </xsl:if>
    <xsl:value-of select="$indent" />
    <xsl:if test="@name">
      <xsl:text>"</xsl:text>
      <xsl:value-of select="@name" />
      <xsl:text>": </xsl:text>
    </xsl:if>
    <xsl:text>{</xsl:text>
    <xsl:if test="*">
      <value-of select="'& #xA;'"/>
      <xsl:apply-templates select="*">
        <xsl:with-param name="indent" select="concat($indent,'  ')" />
      </xsl:apply-templates>
      <value-of select="'& #xA;'"/>
      <xsl:value-of select="$indent" />
    </xsl:if>
    <xsl:text>}</xsl:text>
  </xsl:template>

  <xsl:template match="array">
    <xsl:param name="indent" select="''" />
    <xsl:if test="position() &gt; 1">
      <value-of select="',& #xA;'"/>
    </xsl:if>
    <xsl:value-of select="$indent" />
    <xsl:if test="@name">
      <xsl:text>"</xsl:text>
      <xsl:value-of select="@name" />
      <xsl:text>": </xsl:text>
    </xsl:if>
    <xsl:text>[</xsl:text>
    <xsl:if test="*">
      <value-of select="'& #xA;'"/>
      <xsl:apply-templates select="*">
        <xsl:with-param name="indent" select="concat($indent,'  ')" />
      </xsl:apply-templates>
      <value-of select="'& #xA;'"/>
      <xsl:value-of select="$indent" />
    </xsl:if>
    <xsl:text>]</xsl:text>
  </xsl:template>

  <xsl:template match="str">
    <xsl:param name="indent" select="''" />
    <xsl:if test="position() &gt; 1">
      <value-of select="',& #xA;'"/>
    </xsl:if>
    <xsl:value-of select="$indent" />
    <xsl:if test="@name">
      <xsl:text>"</xsl:text>
      <xsl:value-of select="@name" />
      <xsl:text>": </xsl:text>
    </xsl:if>
    <xsl:text>"</xsl:text>
    <xsl:call-template name="escape">
      <xsl:with-param name="string" select="."/>
    </xsl:call-template>
    <xsl:value-of select="." />
    <xsl:text>"</xsl:text>
  </xsl:template>

  <xsl:template match="num|bool">
    <xsl:param name="indent" select="''" />
    <xsl:if test="position() &gt; 1">
      <value-of select="',& #xA;'"/>
    </xsl:if>
    <xsl:value-of select="$indent" />
    <xsl:if test="@name">
      <xsl:text>"</xsl:text>
      <xsl:value-of select="@name" />
      <xsl:text>": </xsl:text>
    </xsl:if>
    <xsl:value-of select="." />
  </xsl:template>

  <xsl:template match="null">
    <xsl:param name="indent" select="''" />
    <xsl:if test="position() &gt; 1">
      <value-of select="',& #xA;'"/>
    </xsl:if>
    <xsl:value-of select="$indent" />
    <xsl:if test="@name">
      <xsl:text>"</xsl:text>
      <xsl:value-of select="@name" />
      <xsl:text>": </xsl:text>
    </xsl:if>
    <xsl:text>null</xsl:text>
  </xsl:template>

  <xsl:template name="escape">
    <xsl:param name="string"/>
    <xsl:choose>
      <xsl:when test="contains($string,'&quot;')">
        <xsl:call-template name="replace">
          <xsl:with-param name="string" select="$string"/>
          <xsl:with-param name="old" select="'&quot;'"/>
          <xsl:with-param name="new" select="'\&quot;'"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:when test="contains($string,'\')">
        <xsl:call-template name="replace">
          <xsl:with-param name="string" select="$string"/>
          <xsl:with-param name="old" select="'\'"/>
          <xsl:with-param name="new" select="'\\'"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:when test="contains($string,'& #xA;')">
        <xsl:call-template name="replace">
          <xsl:with-param name="string" select="$string"/>
          <xsl:with-param name="old" select="'& #xA;'"/>
          <xsl:with-param name="new" select="'\n'"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:when test="contains($string,'& #xD;')">
        <xsl:call-template name="replace">
          <xsl:with-param name="string" select="$string"/>
          <xsl:with-param name="old" select="'& #xD;'"/>
          <xsl:with-param name="new" select="'\r'"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:when test="contains($string,'& #x9;')">
        <xsl:call-template name="replace">
          <xsl:with-param name="string" select="$string"/>
          <xsl:with-param name="old" select="'& #x9;'"/>
          <xsl:with-param name="new" select="'\t'"/>
        </xsl:call-template>
      </xsl:when>
      <xsl:otherwise>
        <xsl:value-of select="$string"/>
      </xsl:otherwise>
    </xsl:choose>
  </xsl:template>

  <xsl:template name="replace">
    <xsl:param name="string"/>
    <xsl:param name="old"/>
    <xsl:param name="new"/>
    <xsl:call-template name="escape">
      <xsl:with-param name="string" select="substring-before($string,$old)"/>
    </xsl:call-template>
    <xsl:value-of select="$new"/>
    <xsl:call-template name="escape">
      <xsl:with-param name="string" select="substring-after($string,$old)"/>
    </xsl:call-template>
  </xsl:template>

</xsl:transform>

Thumbs up for the ABAP Language Group for providing these useful tools!

PS: you have to replace all occurrences of & #x with &#x in the source code above because I couldn’t figure out how to dissuade this editor from doing multi-pass unencoding 🙁

To report this post you need to login first.

1 Comment

You must be Logged on to comment or reply to a post.

Leave a Reply