3

Working on a Jenkins pipeline, I observed what looks like infinite recursion causing a stack overflow when I use JsonOutput.toJson() on a net.sf.json.JSONObject that slurped a JSON string containing null.

The following minimal code demonstrates the problem:

// Jenkinsfile
@Library('libs@dev') libs

import groovy.json.JsonOutput

pipeline {
  agent any

  stages {
    stage( "json" ) {
      steps {
        script {
          my_lib.to_json_handbuilt_linkedhashmap()
          my_lib.to_json_readjson()
          my_lib.to_json_readjson_as_linkedhashmap()
        }
      }
    }
  }
}
// vars/my_lib.groovy
import groovy.json.JsonOutput

def asMap(j) {
  return j as LinkedHashMap
}

// This function is successful.
def to_json_handbuilt_linkedhashmap() {
  def d = [:]
  d.issues = null

  echo "---- handmade LinkedHashMap ----"
  echo "d ${d}"
  echo "d.getClass() ${d.getClass()}"
  echo "JsonOutput.toJson(d) ${JsonOutput.toJson(d)}"
}

// This function fails from infinite recursion causing a stack overflow.
def to_json_readjson() {
  def d = readJSON(text: '{ "issues" : null }')

  echo "---- readJSON ----"
  echo "d ${d}"
  echo "d.getClass() ${d.getClass()}"
  echo "JsonOutput.toJson(d) ${JsonOutput.toJson(d)}"
}

// This function also fails from infinite recursion causing a stack overflow.
def to_json_readjson_as_linkedhashmap() {
  def d = asMap(readJSON(text: '{ "issues" : null }'))

  echo "---- readJSON -> asMap ----"
  echo "d ${d}"
  echo "d.getClass() ${d.getClass()}"
  echo "JsonOutput.toJson(d) ${JsonOutput.toJson(d)}"
}

In the code above, to_json_readjson() fails with a stack overflow when JsonOutput.toJson() is called with the net.sf.json.JSONObject returned by readJSON(text: '{ "issues" : null }'). The Jenkins console output is at the end of this post.

In to_json_handbuilt_linkedhashmap() JsonOutput.toJson() is successful when called with a handcrafted LinkedHashMap equivalent to { "issues" : null }.

Lastly, in to_json_readjson_as_linkedhashmap(), JsonOutput.toJson() again fails with a stack overflow when called with a LinkedHashMap created from a net.sf.json.JSONObject.

Question:
Can someone please explain what's causing the stack overflow when readJSON() and/or JsonOutput.toJson() are used with a JSON string that has null?

Because my handcrafted LinkedHashMap was successful with JsonOutput.toJson(), I thought the problem was passing JsonOutput.toJson() a net.sf.json.JSONObject.
But I think that theory is ruled out because in to_json_readjson_as_linkedhashmap(), I give JsonOutput.toJson() a LinkedHashMap, albeit created from a net.sf.json.JSONObject.
The problem would appear to be some combination of readJSON() and/or JsonOutput.toJson() that I'm failing to grasp.

I tried, but have given up trying to use a JsonSlurper, because I'm unable to even create an instance of one.

The (truncated) stack overflow error likely showing infinite recursion:

Posting build status of FAILED to bitbucket.company.comjava.lang.StackOverflowError
    at java.io.PrintStream.flush(PrintStream.java:338)
    at sun.nio.cs.StreamEncoder.implFlush(StreamEncoder.java:297)
    at sun.nio.cs.StreamEncoder.flush(StreamEncoder.java:141)
    at java.io.OutputStreamWriter.flush(OutputStreamWriter.java:229)
    at java.util.logging.StreamHandler.flush(StreamHandler.java:259)
    at java.util.logging.ConsoleHandler.publish(ConsoleHandler.java:117)
    at java.util.logging.Logger.log(Logger.java:738)
    at java.util.logging.Logger.doLog(Logger.java:765)
    at java.util.logging.Logger.throwing(Logger.java:1447)
    at org.codehaus.groovy.runtime.DefaultGroovyMethods.getProperties(DefaultGroovyMethods.java:391)
    at groovy.json.JsonOutput.getObjectProperties(JsonOutput.java:327)
    at groovy.json.JsonOutput.writeObject(JsonOutput.java:320)
    at groovy.json.JsonOutput.writeMap(JsonOutput.java:458)
    at groovy.json.JsonOutput.writeObject(JsonOutput.java:321)
    at groovy.json.JsonOutput.writeMap(JsonOutput.java:458)
    at groovy.json.JsonOutput.writeObject(JsonOutput.java:321)
    at groovy.json.JsonOutput.writeMap(JsonOutput.java:458)
    at groovy.json.JsonOutput.writeObject(JsonOutput.java:321)
    at groovy.json.JsonOutput.writeMap(JsonOutput.java:458)
    at groovy.json.JsonOutput.writeObject(JsonOutput.java:321)
    at groovy.json.JsonOutput.writeMap(JsonOutput.java:458)
    at groovy.json.JsonOutput.writeObject(JsonOutput.java:321)
    at groovy.json.JsonOutput.writeMap(JsonOutput.java:458)
    at groovy.json.JsonOutput.writeObject(JsonOutput.java:321)
StoneThrow
  • 5,314
  • 4
  • 44
  • 86

1 Answers1

2

Can you sidestep this immediate problem by using readJSON's returnPojo: true parameter, thereby solving your overall task sooner?

Getting plain old nulls rather than net.sf.json.JSONNull objects really helped me today, though my problem involved producing CSV rather than using JsonOutput.

BillDMoose
  • 52
  • 8
  • Thank you for clueing me in to the `returnPojo` field -- I'd not realized that was an option, and yes: it appears to solve this cleanly and perfectly. – StoneThrow Mar 31 '22 at 15:55