Skip to Content
Author's profile photo Ralf Handl

Pretty-Printing JSON with CALL TRANSFORMATION

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":"to the\nwhole \"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" 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">
      <xsl: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="*">
      <xsl:value-of select="'& #xA;'"/>
      <xsl:apply-templates select="*">
        <xsl:with-param name="indent" select="concat($indent,'  ')" />
      </xsl:apply-templates>
      <xsl: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">
      <xsl: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="*">
      <xsl:value-of select="'& #xA;'"/>
      <xsl:apply-templates select="*">
        <xsl:with-param name="indent" select="concat($indent,'  ')" />
      </xsl:apply-templates>
      <xsl: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">
      <xsl: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:text>"</xsl:text>
  </xsl:template>

  <xsl:template match="num|bool">
    <xsl:param name="indent" select="''" />
    <xsl:if test="position() &gt; 1">
      <xsl: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">
      <xsl: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 🙁

Assigned Tags

      6 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Former Member
      Former Member

      Thank you so much.

      I solved my JSON problem, kar sslc results

      I found it is an useful post for me.

       

      Author's profile photo Olson Matt
      Olson Matt

      Thanks so much for sharing. Exactly what I was hoping to do and works like a charm.

      Author's profile photo Mario Mueller
      Mario Mueller

      Thanks a lot; very good

      Author's profile photo Abinash Nanda
      Abinash Nanda

      This has to be one of the most useful blog on SCN 🙂

      Best regards, Abinash

       

      Author's profile photo Lars Hvam
      Lars Hvam

      Few changes needed after copy-pasting,

      A: "&gt;" should be replaced with ">"

      B: "& #xA;" are hex escaped HTML values, and should not contain spaces, ie replace with " "

      Author's profile photo Sandra Rossi
      Sandra Rossi

      There's also the built-in options in SXML, see jsonPrettyPrinter by Andre. Shortest possible code (debug to see the result) - 1 character indentation per level - all JSON & code below credited to Andre blog post:

          DATA(json_string) = `{ "firstName": "John", "lastName": "Smith", "isAlive": true, "age": 27,` && |\r\n|  &&
                        `  "address": { "streetAddress": "21 2nd Street", "city": "New York", "state": "NY",` && |\r\n|  &&
                        `    "postalCode": "10021-3100" }, "phoneNumbers": [  { "type": "home",` && |\r\n|  &&
                        `      "number": "212 555-1234"  },  { "type": "office",  "number": "646 555-4567" }   ],` && |\r\n|  &&
                        `  "children": [],   "spouse": null }`.
          DATA(reader) = cl_sxml_string_reader=>create( cl_abap_codepage=>convert_to( json_string ) ).
          DATA(writer) = CAST if_sxml_writer( cl_sxml_string_writer=>create( type = if_sxml=>co_xt_json ) ).
          writer->set_option( option = if_sxml_writer=>co_opt_linebreaks ).
          writer->set_option( option = if_sxml_writer=>co_opt_indent ).
          reader->next_node( ).
          reader->skip_node( writer ).
          DATA(formatted_json_string) = cl_abap_codepage=>convert_from( CAST cl_sxml_string_writer( writer )->get_output( ) ).
      

      (Lars Hvam fyi if needed)