Solving Jenkins Pipeline NotSerializableException: groovy.json.internal.LazyMap

Updated: February 4, 2024 By: Guest Contributor Post a comment

Overview

If you have come across a NotSerializableException error when working with Jenkins pipelines, specifically with the groovy.json.internal.LazyMap class, don’t worry—you’re not alone. This problem commonly arises when trying to serialize Groovy’s lazy maps within a pipeline, typically during the serialization/deserialization process that Jenkins uses to save pipeline state across different nodes.

Understanding the Error

The NotSerializableException in Jenkins pipelines primarily occurs because Jenkins requires every object that persists across different stages or nodes to be serializable. However, Groovy’s LazyMap, which is often used to parse JSON responses easily, does not implement the java.io.Serializable interface, thus causing this exception.

Let’s Fix It

Solution #1 – Use @NonCPS Annotation

By marking methods that manipulate non-serializable objects like LazyMap with the @NonCPS annotation, you can prevent them from being serialized. This solution forces Jenkins to treat these methods differently, avoiding the serialization process for their execution.

  • Step 1: Identify methods that use or manipulate LazyMap.
  • Step 2: Add the @NonCPS annotation to these methods.

Code example:

@NonCPS
def parseJson(String json) {
  return new groovy.json.JsonSlurper().parseText(json)
}

Notes: This approach has its limitations, such as not being able to use certain pipeline features within the @NonCPS methods. However, it’s an easy fix for simple scripts.

Solution #2 – Convert LazyMap to Serializable Map

Another way to fix this issue is by converting the LazyMap into a standard Java map or any other structure that implements the Serializable interface before using it in a context that requires serialization.

Code example:

def json = new groovy.json.JsonSlurper().parseText(jsonString)
def serializableMap = new HashMap<>(json)

Notes: This solution may introduce some overhead but guarantees that your object is serializable, thus avoiding the NotSerializableException.

Solution #3 – Use Scripted Pipeline Instead


To address serialization issues such as NotSerializableException in Jenkins pipelines, especially when working with non-serializable objects like groovy.json.internal.LazyMap, switching to a scripted pipeline can provide a workaround. Scripted pipelines offer more granular control over the script’s execution context and can handle complex logic that might not be serializable in a declarative pipeline. Here’s an example of how you can rewrite a declarative pipeline into a scripted pipeline:

Declarative Pipeline (Original)

Assuming you have a declarative pipeline that’s causing a NotSerializableException due to the handling of a JSON response:

pipeline {
    agent any
    stages {
        stage('Process JSON') {
            steps {
                script {
                    def jsonResponse = sh(script: 'curl -s http://example.com/api/data', returnStdout: true).trim()
                    def jsonData = readJSON text: jsonResponse
                    echo "Data: ${jsonData.key}"
                }
            }
        }
    }
}

Scripted Pipeline (Rewritten to Avoid Serialization Issues)

Here’s how you could rewrite the above logic using a scripted pipeline:

node {
    // Define stages
    stage('Checkout') {
        checkout scm
    }
    
    stage('Process JSON') {
        // Wrap potentially non-serializable steps in a 'node' block
        node {
            // Shell step to get JSON response
            def jsonResponse = sh(script: 'curl -s http://example.com/api/data', returnStdout: true).trim()
            // Use Groovy's JsonSlurper for parsing JSON which doesn't need to be serialized
            def jsonSlurper = new groovy.json.JsonSlurper()
            def jsonData = jsonSlurper.parseText(jsonResponse)
            
            // Non-serializable objects are handled within this block
            echo "Data: ${jsonData.key}"
        }
    }
}

In this scripted pipeline:

  • The main logic is wrapped within a node block, which specifies that the enclosed steps should run on an agent.
  • The stage directive is used similarly to the declarative pipeline to define stages of the pipeline.
  • The Groovy JsonSlurper is used for parsing the JSON response instead of the readJSON step. This is because JsonSlurper does not introduce serialization issues.
  • Non-serializable objects like the result of JsonSlurper.parseText() are handled within the node block, avoiding the NotSerializableException.

By restructuring your pipeline into a scripted format, you can utilize Groovy’s features more flexibly to manage non-serializable objects and control the execution flow to work around serialization issues.

Conclusion

While encountering a NotSerializableException within Jenkins pipelines can be frustrating, understanding the root causes and applying appropriate fixes can help you overcome this hurdle. Whether you choose to use the @NonCPS annotation, convert non-serializable objects, or switch pipeline types, the key is to understand your pipeline’s needs and choose the solution that best fits your situation.