Serialization error with lambdas and up-to-date checks

I’m trying to write a custom task that merges objects from multiple files. Each file contains a number of objects, and when configuring the task I want to be able to specify (1) what files to retrieve the objects from, and (2) a function that processes the retrieved objects.

However, I’m unsure how best to go about writing a task like this, and my current attempt seems to fail with a serialization error when trying to determine if the task is up-to-date.

Below is a simple proof of concept build.gradle.kts outlining the general idea of the task.

data class Data(
        var name: String,
        var value: String,
        var num: Int
)

open class MyTask : DefaultTask() {
    // map of lists of Data; normally each list is a separate JSON file
    @get:Internal
    val sourceData = mapOf(
            "test" to listOf(
                    Data("hello", "value1", 1),
                    Data("test", "value2", 10)
            ),
            "test2" to listOf(
                    Data("testing", "value3", 5)
            ),
            "test3" to listOf(
                    Data("testing more", "value4", 5)
            )
    )

    @get:Input
    val dataSpecifiers = mutableListOf<Pair<String, Data.() -> Unit>>()

    @get:Internal
    lateinit var data: List<Data>

    // dummy output to trigger gradle's up-to-date check
    @get:OutputDirectory
    val out = project.layout.buildDirectory

    fun addData(source: String, action: Data.() -> Unit) {
        dataSpecifiers.add(Pair(source, action))
    }

    @TaskAction
    fun run() {
        // retrieve the specified data objects and process them
        // using the provided lambdas
        data = dataSpecifiers.flatMap { (source, action) ->
            sourceData[source]?.map {
                it.action()
                it
            } ?: listOf()
        }

        // do things with data
        println(data)
    }
}

val myTask by tasks.registering(MyTask::class) {
    addData("test2") {
        if (value == "value1") {
            num *= 5
        }
    }

    addData("test3") {

    }
}

Running this task once will print the processed data as expected, and running it again will result in Gradle correctly detecting it as up-to-date. Further, if I change num *= 5 to e.g. num *= 10, Gradle will rerun the task, just like I want.

However, if I change the first argument of addData from "test" to e.g. "test2", I get the following error:

$ gradle myTask --stacktrace
> Task :myTask FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':myTask'.
> Unable to store input properties for task ':myTask'. Property 'dataSpecifiers' with value '[(test2, Build_gradle.Data.() -> kotlin.Unit), (test3, Build_gradle.Data.() -> kotlin.Unit)]' cannot be serialized.

* Try:
Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':myTask'.
        at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:38)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor$CallableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:416)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor$CallableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:406)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor$1.execute(DefaultBuildOperationExecutor.java:165)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:250)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:158)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:102)
        at org.gradle.internal.operations.DelegatingBuildOperationExecutor.call(DelegatingBuildOperationExecutor.java:36)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:52)
        at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:41)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:372)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:359)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:352)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:338)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.lambda$run$0(DefaultPlanExecutor.java:127)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:191)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.executeNextNode(DefaultPlanExecutor.java:182)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:124)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)
        at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:48)
        at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:56)
Caused by: org.gradle.api.UncheckedIOException: Unable to store input properties for task ':myTask'. Property 'dataSpecifiers' with value '[(test2, Build_gradle.Data.() -> kotlin.Unit), (test3, Build_gradle.Data.() -> kotlin.Unit)]' cannot be serialized.
        at org.gradle.internal.execution.steps.CaptureStateBeforeExecutionStep.lambda$fingerprintInputProperties$2(CaptureStateBeforeExecutionStep.java:178)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$TaskExecution.visitInputProperties(ExecuteActionsTaskExecuter.java:307)
        at org.gradle.internal.execution.steps.CaptureStateBeforeExecutionStep.fingerprintInputProperties(CaptureStateBeforeExecutionStep.java:169)
        at org.gradle.internal.execution.steps.CaptureStateBeforeExecutionStep.captureExecutionState(CaptureStateBeforeExecutionStep.java:149)
        at org.gradle.internal.execution.steps.CaptureStateBeforeExecutionStep.lambda$captureExecutionStateOp$1(CaptureStateBeforeExecutionStep.java:104)
        at org.gradle.internal.execution.steps.BuildOperationStep$1.call(BuildOperationStep.java:40)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor$CallableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:416)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor$CallableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:406)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor$1.execute(DefaultBuildOperationExecutor.java:165)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:250)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:158)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:102)
        at org.gradle.internal.operations.DelegatingBuildOperationExecutor.call(DelegatingBuildOperationExecutor.java:36)
        at org.gradle.internal.execution.steps.BuildOperationStep.operation(BuildOperationStep.java:37)
        at org.gradle.internal.execution.steps.CaptureStateBeforeExecutionStep.captureExecutionStateOp(CaptureStateBeforeExecutionStep.java:103)
        at org.gradle.internal.execution.steps.CaptureStateBeforeExecutionStep.lambda$execute$0(CaptureStateBeforeExecutionStep.java:78)
        at org.gradle.internal.execution.steps.CaptureStateBeforeExecutionStep.execute(CaptureStateBeforeExecutionStep.java:78)
        at org.gradle.internal.execution.steps.CaptureStateBeforeExecutionStep.execute(CaptureStateBeforeExecutionStep.java:53)
        at org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:74)
        at org.gradle.internal.execution.steps.SkipEmptyWorkStep.lambda$execute$2(SkipEmptyWorkStep.java:78)
        at org.gradle.internal.execution.steps.SkipEmptyWorkStep.execute(SkipEmptyWorkStep.java:78)
        at org.gradle.internal.execution.steps.SkipEmptyWorkStep.execute(SkipEmptyWorkStep.java:34)
        at org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsStartedStep.execute(MarkSnapshottingInputsStartedStep.java:39)
        at org.gradle.internal.execution.steps.LoadExecutionStateStep.execute(LoadExecutionStateStep.java:40)
        at org.gradle.internal.execution.steps.LoadExecutionStateStep.execute(LoadExecutionStateStep.java:28)
        at org.gradle.internal.execution.impl.DefaultWorkExecutor.execute(DefaultWorkExecutor.java:33)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:192)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:184)
        at org.gradle.api.internal.tasks.execution.CleanupStaleOutputsExecuter.execute(CleanupStaleOutputsExecuter.java:114)
        at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:46)
        at org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter.execute(ResolveTaskExecutionModeExecuter.java:62)
        at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:57)
        at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:56)
        at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
        ... 23 more
Caused by: org.gradle.internal.snapshot.ValueSnapshottingException: Couldn't populate class kotlin.Pair
        at org.gradle.internal.snapshot.impl.SerializedValueSnapshot.populateClass(SerializedValueSnapshot.java:108)
        at org.gradle.internal.snapshot.impl.SerializedValueSnapshot.hasSameSerializedValue(SerializedValueSnapshot.java:72)
        at org.gradle.internal.snapshot.impl.SerializedValueSnapshot.snapshot(SerializedValueSnapshot.java:53)
        at org.gradle.internal.snapshot.impl.DefaultValueSnapshotter.snapshot(DefaultValueSnapshotter.java:59)
        at org.gradle.internal.snapshot.impl.ListValueSnapshot.processList(ListValueSnapshot.java:49)
        at org.gradle.internal.snapshot.impl.ListValueSnapshot.snapshot(ListValueSnapshot.java:34)
        at org.gradle.internal.snapshot.impl.DefaultValueSnapshotter.snapshot(DefaultValueSnapshotter.java:59)
        at org.gradle.internal.execution.steps.CaptureStateBeforeExecutionStep.lambda$fingerprintInputProperties$2(CaptureStateBeforeExecutionStep.java:175)
        ... 56 more
Caused by: java.lang.ClassNotFoundException: Build_gradle$myTask$2$1
        at org.gradle.internal.io.ClassLoaderObjectInputStream.resolveClass(ClassLoaderObjectInputStream.java:40)
        at org.gradle.internal.snapshot.impl.SerializedValueSnapshot.populateClass(SerializedValueSnapshot.java:106)
        ... 63 more


* Get more help at https://help.gradle.org

BUILD FAILED in 345ms
1 actionable task: 1 executed

Is there any way to make this work as I’d like?