🔥 🔥Practical Open Source is coming 🔥🔥 Propose an article about doing business with Open Source!

Tame JSON and XML with Apache Groovy

Apache Groovy’s JsonSlurper and XmlSlurper classes streamline JSON and XML parsing, boosting your productivity.

We’ll look at what Groovy provides for reading and parsing JSON and XML. If you missed the last tutorial, check it out or take a look at the whole series.

Let’s start with JavaScript Object Notation, better known as JSON.

The Groovy package groovy.json contains a class JsonSlurper that is very easy to use to ingest JSON.

Most Linux installations provide lots of example JSON files; in my case, I found a moderately complicated one in /usr/share/iso-codes/json/schema-15924.json that looks like this:

{
  "$schema": "http://json-schema.org/draft-04/schema#",

  "title": "ISO 15924",
  "description": "Codes for the representation of names of scripts",
  "type": "object",

  "properties": {
	"15924": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "alpha_4": {
            "description": "Four letter alphabetic code of the script",
            "type": "string",
            "pattern": "^[A-Z][a-z]{3}$"
          },
          "name": {
            "description": "Name of the script",
            "type": "string",
            "minLength": 1
          },
          "numeric": {
            "description": "Three digit numeric code of the script, including leading zeros",
            "type": "string",
            "pattern": "^[0-9]{3}$"
          }
        },
        "required": ["alpha_4", "name", "numeric"],
        "additionalProperties": false
      }
    }
  },
  "additionalProperties": false
}

Here’s a simple Groovy program to parse that file and write it back out in a reasonably attractive form:

1   import groovy.json.JsonSlurper
2   def jsonFileName = '/usr/share/iso-codes/json/schema-15924.json'
3   def jsonMap = new JsonSlurper().parse(new File(jsonFileName))
4   walkPrint(jsonMap, '')
5   def walkPrint(json,ldr) {
6       json.each { k, v ->
7      print "$ldr$k: "
8      if (v == null) {
9     print "(null) "
10      } else if (v instanceof String) {
11     print "'$v' "
12      } else if (v instanceof BigDecimal || v instanceof Integer || v instanceof Boolean || v instanceof ArrayList || v instanceof Date) {
13     print "$v "
14      } else {
15     println "["
16     walkPrint(v,ldr + '    ')
17     print "$ldr] "
18      }
19      println "(${v.getClass()})"
20     }
21   }

Line one imports the JsonSlurper class.

Line two declares the file name.

Line three instantiates a File object associated with that file name and then uses the parse() method of JsonSlurper to parse it.

Lines four-20 print the parsed JSON.

When we run the above program, the output looks like this:

$ groovy Groovy21a.groovy
$schema: 'http://json-schema.org/draft-04/schema#' (class java.lang.String)
title: 'ISO 15924' (class java.lang.String)
description: 'Codes for the representation of names of scripts' (class java.lang.String)
type: 'object' (class java.lang.String)
properties: [
  15924: [
    type: 'array' (class java.lang.String)
    items: [
      type: 'object' (class java.lang.String)
      properties: [
        alpha_4: [
          description: 'Four letter alphabetic code of the script' (class java.lang.String)
          type: 'string' (class java.lang.String)
          pattern: '^[A-Z][a-z]{3}$' (class java.lang.String)
        ] (class org.apache.groovy.json.internal.LazyMap)
        name: [
          description: 'Name of the script' (class java.lang.String)
          type: 'string' (class java.lang.String)
          minLength: 1 (class java.lang.Integer)
        ] (class org.apache.groovy.json.internal.LazyMap)
        numeric: [
          description: 'Three digit numeric code of the script, including leading zeros' (class java.lang.String)
          type: 'string' (class java.lang.String)
          pattern: '^[0-9]{3}$' (class java.lang.String)
        ] (class org.apache.groovy.json.internal.LazyMap)
      ] (class org.apache.groovy.json.internal.LazyMap)
      required: [alpha_4, name, numeric] (class java.util.ArrayList)
      additionalProperties: false (class java.lang.Boolean)
    ] (class org.apache.groovy.json.internal.LazyMap)
  ] (class org.apache.groovy.json.internal.LazyMap)
] (class org.apache.groovy.json.internal.LazyMap)
additionalProperties: false (class java.lang.Boolean)

Take note:

  • JsonSlurper converts the JSON to a Groovy Map (actually, as we can see, a LazyMap). Recall that Map instances are key-value pairs, which makes dealing with the JSON object in Groovy quite straightforward.
  • In the pretty printer code in method walkPrint(), we see that values can be simple(ish), like null, String, Integer, BigDecimal, Date, Boolean; or ArrayList; or something else, which turns out to be the LazyMap. This is explained further in the Groovy documentation on parsing JSON, which also explains more details on variants of the JsonSlurper.parse() method that take arguments like String.
  • In the latest Groovy / Java environment, which supports Java’s pattern matching switch expression, we could use a switch expression in place of the if / else statements with instanceof.
  • We can use Groovy’s GPath expression language to get at sub parts of the JSON structure. This is in the case where the program knows something about the structure and wants to reason with it, for example, if the JSON is configuration information. So in the above JSON file, we could refer to jsonMap.additionalProperties, which would have the value false, or jsonMap.properties.'15924'.type, which would have the value ‘array’ for example (note that 15924 must be quoted here since it isn’t a legal name in Groovy).

Read XML in Groovy

Handling XML is quite similar. In particular, there’s a package groovy.xml that contains a class XmlSlurper that bears marked similarity to JsonSlurper.

A file I found on my system at /usr/share/help-langpack/es/rhythmbox/legal.xml looks like a good test case.
Here’s what it looks like:

<?xml version="1.0" encoding="utf-8"?>
<legalnotice id="legalnotice">
	<para>Se concede permiso para copiar, distribuir o modificar este documento según las condiciones de la GNU Free Documentation License (GFDL), Versión 1.1 o cualquier versión posterior publicada por la Free Software Foundation sin Secciones invariantes, Textos de portada y Textos de contraportada. Encontrará una copia de la GFDL en este <ulink type="help" url="help:fdl">enlace</ulink> o en el archivo COPYING-DOCS distribuido con este manual.</para>
         <para>Este manual es parte de una colección de manuales de GNOME distribuido bajo la GFDL. Si quiere distribuir este manual por separado de la colección, puede hacerlo añadiendo una copia de la licencia al manual, tal como se describe en la sección 6 de la licencia.</para>

	<para>Muchos de los nombres usados por compañías para distinguir sus productos y servicios son mencionados como marcas comerciales. Donde esos nombres aparezcan en cualquier documentación de GNOME, y los miembros del Proyecto de Documentación de GNOME están al corriente de esas marcas comerciales, entonces los nombres se pondrán en mayúsculas o con la inicial en mayúsculas.</para>

	<para>ESTE DOCUMENTO Y LAS VERSIONES MODIFICADAS DEL MISMO SE PROPORCIONAN SEGÚN LAS CONDICIONES ESTABLECIDAS EN LA LICENCIA DE DOCUMENTACIÓN LIBRE DE GNU (GFDL) Y TENIENDO EN CUENTA QUE: <orderedlist>
		<listitem>
		  <para>EL DOCUMENTO SE PROPORCIONA "TAL CUAL", SIN GARANTÍA DE NINGÚN TIPO, NI EXPLÍCITA NI IMPLÍCITA INCLUYENDO, SIN LIMITACIÓN, GARANTÍA DE QUE EL DOCUMENTO O VERSIÓN MODIFICADA DE ÉSTE CAREZCA DE DEFECTOS COMERCIALES, SEA ADECUADO A UN FIN CONCRETO O INCUMPLA ALGUNA NORMATIVA. TODO EL RIESGO RELATIVO A LA CALIDAD, PRECISIÓN Y UTILIDAD DEL DOCUMENTO O SU VERSIÓN MODIFICADA RECAE EN USTED. SI CUALQUIER DOCUMENTO O VERSIÓN MODIFICADA DE AQUÉL RESULTARA DEFECTUOSO EN CUALQUIER ASPECTO, USTED (Y NO EL REDACTOR INICIAL, AUTOR O CONTRIBUYENTE) ASUMIRÁ LOS COSTES DE TODA REPARACIÓN, MANTENIMIENTO O CORRECCIÓN NECESARIOS. ESTA RENUNCIA DE GARANTÍA ES UNA PARTE ESENCIAL DE ESTA LICENCIA. NO SE AUTORIZA EL USO DE NINGÚN DOCUMENTO NI VERSIÓN MODIFICADA DE ÉSTE POR EL PRESENTE, SALVO DENTRO DEL CUMPLIMIENTO DE LA RENUNCIA;Y</para>
		</listitem>
		<listitem>
		  <para>BAJO NINGUNA CIRCUNSTANCIA NI BAJO NINGUNA TEORÍA LEGAL, SEA POR ERROR (INCLUYENDO NEGLIGENCIA), CONTRATO O DE ALGÚN OTRO MODO, EL AUTOR, EL ESCRITOR INICIAL, CUALQUIER CONTRIBUIDOR, O CUALQUIER DISTRIBUIDOR DEL DOCUMENTO O VERSIÓN MODIFICADA DEL DOCUMENTO, O CUALQUIER PROPORCIONADOR DE CUALQUIERA DE ESAS PARTES, SERÁ RESPONSABLE ANTE NINGUNA PERSONA POR NINGÚN DAÑO DIRECTO, INDIRECTO, ESPECIAL, INCIDENTAL O DERIVADO DE NINGÚN TIPO, INCLUYENDO, SIN LIMITACIÓN DAÑOS POR PÉRDIDA DE MERCANCÍAS, PARO TÉCNICO, FALLO INFORMÁTICO O MAL FUNCIONAMIENTO O CUALQUIER OTRO POSIBLE DAÑO O PÉRDIDAS DERIVADAS O RELACIONADAS CON EL USO DEL DOCUMENTO O SUS VERSIONES MODIFICADAS, AUNQUE DICHA PARTE HAYA SIDO INFORMADA DE LA POSIBILIDAD DE QUE SE PRODUJESEN DICHOS DAÑOS.</para>
		</listitem>
	  </orderedlist></para>
  </legalnotice>

Here’s a simple Groovy program to parse and traverse that XML file:

1   import groovy.xml.XmlSlurper
2   def xmlFileName = '/usr/share/help-langpack/es/rhythmbox/legal.xml'
3   def xmlMap = new XmlSlurper().parse(new File(xmlFileName))
4   walkPrint(xmlMap, '')
5   def walkPrint(xml,ldr) {
6       println "${ldr}name ${xml.name()} attributes ${xml.attributes()} localText ${xml.localText()}"
7       xml.'*'.each { c ->
8      walkPrint(c, ldr + '    ')
9       }
10   }

Line one imports the XmlSlurper class.

Line two declares the file name.

Line three instantiates a File object associated with that file name and then uses the parse() method of XmlSlurper to parse it.

Lines four to nine print the parsed XML. Note that the structure returned by the parsing is different than for the JSON case. In particular, the structure is a tree of groovy.xml.slurpersupport.NodeChild elements, which is a class that extends groovy.xml.slurpersupport.GPathResult. These elements are explained in further detail in the Groovy programming manual and in the reference documentation for the NodeChild class. We make use of three methods to print out the tree details:

  • name() yields the name of the node, such as “para” in our case
  • attributes() yields a map of the attributes of the node, if any
  • localText() yields a list of any text strings directly associated with the node

We also use the Groovy shorthand of an asterisk (*) to refer to the children() of the current node using the GPath language syntax.

When we run this, we see:

$ groovy Groovy21b.groovy
name legalnotice attributes [id:legalnotice] localText [] name para attributes [:] localText [Se concede permiso para copiar, distribuir o modificar este documento según las condiciones de la GNU Free Documentation License (GFDL), Versión 1.1 o cualquier versión posterior publicada por la Free Software Foundation sin Secciones invariantes, Textos de portada y Textos de contraportada. Encontrará una copia de la GFDL en este , o en el archivo COPYING-DOCS distribuido con este manual.] name ulink attributes [type:help, url:help:fdl] localText [enlace] name para attributes [:] localText [Este manual es parte de una colección de manuales de GNOME distribuido bajo la GFDL. Si quiere distribuir este manual por separado de la colección, puede hacerlo añadiendo una copia de la licencia al manual, tal como se describe en la sección 6 de la licencia.] name para attributes [:] localText [Muchos de los nombres usados por compañías para distinguir sus productos y servicios son mencionados como marcas comerciales. Donde esos nombres aparezcan en cualquier documentación de GNOME, y los miembros del Proyecto de Documentación de GNOME están al corriente de esas marcas comerciales, entonces los nombres se pondrán en mayúsculas o con la inicial en mayúsculas.] name para attributes [:] localText [ESTE DOCUMENTO Y LAS VERSIONES MODIFICADAS DEL MISMO SE PROPORCIONAN SEGÚN LAS CONDICIONES ESTABLECIDAS EN LA LICENCIA DE DOCUMENTACIÓN LIBRE DE GNU (GFDL) Y TENIENDO EN CUENTA QUE: ] name orderedlist attributes [:] localText [] name listitem attributes [:] localText [] name para attributes [:] localText [EL DOCUMENTO SE PROPORCIONA "TAL CUAL", SIN GARANTÍA DE NINGÚN TIPO, NI EXPLÍCITA NI IMPLÍCITA INCLUYENDO, SIN LIMITACIÓN, GARANTÍA DE QUE EL DOCUMENTO O VERSIÓN MODIFICADA DE ÉSTE CAREZCA DE DEFECTOS COMERCIALES, SEA ADECUADO A UN FIN CONCRETO O INCUMPLA ALGUNA NORMATIVA. TODO EL RIESGO RELATIVO A LA CALIDAD, PRECISIÓN Y UTILIDAD DEL DOCUMENTO O SU VERSIÓN MODIFICADA RECAE EN USTED. SI CUALQUIER DOCUMENTO O VERSIÓN MODIFICADA DE AQUÉL RESULTARA DEFECTUOSO EN CUALQUIER ASPECTO, USTED (Y NO EL REDACTOR INICIAL, AUTOR O CONTRIBUYENTE) ASUMIRÁ LOS COSTES DE TODA REPARACIÓN, MANTENIMIENTO O CORRECCIÓN NECESARIOS. ESTA RENUNCIA DE GARANTÍA ES UNA PARTE ESENCIAL DE ESTA LICENCIA. NO SE AUTORIZA EL USO DE NINGÚN DOCUMENTO NI VERSIÓN MODIFICADA DE ÉSTE POR EL PRESENTE, SALVO DENTRO DEL CUMPLIMIENTO DE LA RENUNCIA;Y] name listitem attributes [:] localText [] name para attributes [:] localText [BAJO NINGUNA CIRCUNSTANCIA NI BAJO NINGUNA TEORÍA LEGAL, SEA POR ERROR (INCLUYENDO NEGLIGENCIA), CONTRATO O DE ALGÚN OTRO MODO, EL AUTOR, EL ESCRITOR INICIAL, CUALQUIER CONTRIBUIDOR, O CUALQUIER DISTRIBUIDOR DEL DOCUMENTO O VERSIÓN MODIFICADA DEL DOCUMENTO, O CUALQUIER PROPORCIONADOR DE CUALQUIERA DE ESAS PARTES, SERÁ RESPONSABLE ANTE NINGUNA PERSONA POR NINGÚN DAÑO DIRECTO, INDIRECTO, ESPECIAL, INCIDENTAL O DERIVADO DE NINGÚN TIPO, INCLUYENDO, SIN LIMITACIÓN DAÑOS POR PÉRDIDA DE MERCANCÍAS, PARO TÉCNICO, FALLO INFORMÁTICO O MAL FUNCIONAMIENTO O CUALQUIER OTRO POSIBLE DAÑO O PÉRDIDAS DERIVADAS O RELACIONADAS CON EL USO DEL DOCUMENTO O SUS VERSIONES MODIFICADAS, AUNQUE DICHA PARTE HAYA SIDO INFORMADA DE LA POSIBILIDAD DE QUE SE PRODUJESEN DICHOS DAÑOS.] 
$

There are other options for processing XML which are documented in the references above. And the same situation we saw in JSON with using GPath notation to access known child nodes holds with XML files. However, the typical use of XML, seen above, is to have many similar elements (see <para> above) and therefore in XML the index of the elements comes into play. That is, the first <para> element (element number 0) in the above example has localText() “Se concede permiso para copiar…”, and we would use xmlMap.para[0].localText() to access it; similarly for the second, etc. The method xmlMap.para.size() tells us how many elements of name ‘para’ exist in the structure.

That’s it!

Conclusion

One line of Groovy code: That’s all it takes to parse JSON or XML into a usable structure. Groovy slurpers: powerful and concise.

Disclaimer: All published articles represent the views of the authors, they don’t represent the official positions of the Open Source Initiative, even if the authors are OSI staff members or board directors.

Author

Support us

OpenSource.net is supported by the Open Source Initiative, the non-profit organization that defines Open Source.

Trending