4.1.12  JSON mit XSLT 1.0 und Python lxml

Wenn man mit Python programmiert, hat man leider nicht Saxon als XSLT Prozessor zur verfügung, sondern muss sich mit dem XSLT Prozessor der lxml Bibliothek begnügen. Diese basiert auf der C Bibliothek libxslt

Dabei steht nur XSLT 1.0 zur Verfügung mit den EXSLT Erweiterungen.

Unter diesen Bedingungen war es nicht ganz einfach eine Transformation zu erstellen, die XML als Eingabe nimmt und sauberes JSON als Ausgabe produziert. Sicherlich ist hierfür ein mehrstufiger Prozess erforderlich.

Um genau zu sein braucht man mindestens eine Vortransformation, die die JSON Elemente als XML erzeugt und ein Postprocessing, das möglichst generisch die spitzen Klammern auf geschweifte abbildet und einige Elementnamen verwirft.

Die folgenden JSON Strukturen lassen sich leicht identifizieren:

1. Properties

"propertyName" : propertyValue
Plain Text

1. Dabei kann der propertyValue vom Typ String, Number oder Boolean sein.

2. Benannte Objekte

"objektName" : { ... }
Plain Text

3. Bennante Listen

"listenName" : [ { ... }, { ... }]
Plain Text

4. Anonyme Objekte

{ }
Plain Text

In Objekten können Properties, benannte Objekte oder benannte Listen enthalten sein. In benannten Listen können nur anonyme Objekte enthalten sein. Diese Unterscheidung hat erst einmal für meinen Anwendungsfall ausgereicht.

Umgemünzt auf XSLT Regeln, kommt man auf vier Regeln die anhand einer Attributbelegung für @json matchen:

<xsl:template match="*[@json='anonymous-object']" mode="post">
  <xsl:text>{</xsl:text>
  <xsl:apply-templates select="node()|@*" mode="post"/>
  <xsl:text>}</xsl:text>
  <xsl:if test="following-sibling::*">,</xsl:if>
</xsl:template>

<xsl:template match="*[@json='named-list']" mode="post">
  <xsl:text>"</xsl:text>
  <xsl:value-of select="name()"/>
  <xsl:text>"</xsl:text>
  <xsl:text>:[</xsl:text>
  <xsl:apply-templates select="node()|@*" mode="post"/>
  <xsl:text>]</xsl:text>
  <xsl:if test="following-sibling::*">,</xsl:if>
</xsl:template>

<xsl:template match="*[@json='named-object']" mode="post">
  <xsl:text>"</xsl:text>
  <xsl:value-of select="name()"/>
  <xsl:text>"</xsl:text>
  <xsl:text>:{</xsl:text>
  <xsl:apply-templates select="node()|@*" mode="post"/>
  <xsl:text>}</xsl:text>
  <xsl:if test="following-sibling::*">,</xsl:if>
</xsl:template>

<xsl:template match="*[@json='property']" mode="post">
  <xsl:text>"</xsl:text>
  <xsl:value-of select="name()"/>
  <xsl:choose>
    <xsl:when test="@type='boolean' or @type='integer'">
      <xsl:text>":</xsl:text>
      <xsl:value-of select="."/>
    </xsl:when>
    <xsl:otherwise>
      <xsl:text>":"</xsl:text>
      <xsl:value-of select="."/>
      <xsl:text>"</xsl:text>
    </xsl:otherwise>
  </xsl:choose>
  <xsl:if test="following-sibling::*">,</xsl:if>
</xsl:template>
Plain Text

In einer Vortransformation in eine Variable pre werden die JSON Strukturen erstmal als XML aufgebaut und dann in einem Postprocessing Step post auf die geschweiften Klammern gemappt:

<xsl:template match="/">
  <root>
    <!-- first processing step -->
    <xsl:variable name="pre">
      <xsl:copy>
        <xsl:apply-templates/>
      </xsl:copy>
    </xsl:variable>
    <!-- second processing step -->
    <xsl:apply-templates select="exsl:node-set($pre)" mode="post"/>
  </root>
</xsl:template>
Plain Text

Das Aufbauen der XML-artigen JSON Struktur mit den annotierten @json Attributen, könnte z.B. so aussehen:

<xsl:template match="testsuite">
  <sarif-report json="anonymous-object">
    <runs json="named-list">
      <run json="anonymous-object">
        [...]
      </run>
    </runs>
    <version json="property">2.1.0</version>
  </sarif-report>
</xsl:template>
Plain Text

Mit folgender finaler Ausgabe:

{ "runs" : [{ ... }],
  "version": "2.1.0"
}
Plain Text

Da Python lxml bzw. libxslt anscheinend kein xsl:output method="text" beherscht, muss im Python Code der Textknoten eines Root-Elements ausgelesen werden. Wenn man diesen als JSON in ein Python dictionary liest und anschliessend wieder als JSON mit Indention herausschreibt, hat man auch gleich ein schönes Pretty-Printing der JSON Strukturen. Der relevante Python Code sieht dabei so aus:

with fileobj:
  # We register namespace functions that can be called inside the XSLT transformation
  register_stylesheet_functions()
  # Read the XSLT stylesheet
  xsl = etree.XML(open(SARIF_TRANSFORMATION_FILE, "r", encoding="utf-8").read())
  # Transform the XML to Sarif JSON and put it into the root text node
  transform = etree.XSLT(xsl)
  result = transform(tree)
  if INDENT:
    # Load into python dict and pretty print when writing back to JSON
    jsn = json.loads(result.getroot().text)
    fileobj.write(json.dumps(jsn, indent=2))
    else:
      fileobj.write(result.getroot().text)
Plain Text

Das vollständige Beispiel findet man in meinem Github Repo.