Is it necessary to make a DSL nested object implements `Serializable`?

I have this plugin DSL,

abstract class MuzzleExtension @Inject constructor(private val objectFactory: ObjectFactory) {
    // ...
    fun pass(action: Action<in MuzzleDirective>) {
        val pass = objectFactory.newInstance(MuzzleDirective::class.java)
        action.execute(pass)
        postConstruct(pass)
        pass.assertPass = true
        directives.add(pass)
    }
    //...
}
open class MuzzleDirective {
  // var name:String? = null
  var additionalDeps: MutableList<String> = ArrayList()
  // ... no gradle properties
}

the directive is later added to a task input

  @get:Input
  @get:Optional
  val muzzleDirective: Property<MuzzleDirective> = objects.property()

And when running the task I noticed errors like that

* What went wrong:
Execution failed for task ':dd-java-agent:instrumentation:jetty-9:muzzle-AssertFail-org.eclipse.jetty-jetty-server-8.2.0.v20160908-between-10-and-12'.
> Cannot fingerprint input property 'muzzleDirective': value 'fail org.eclipse.jetty:jetty-server:8.2.0.v20160908' cannot be serialized.

The error is pretty straightforward especially when looking at the stacktrace:

Caused by: org.gradle.internal.snapshot.ValueSnapshottingException: Could not serialize value of type MuzzleDirective
	at ...
    ... 72 more
Caused by: java.io.NotSerializableException: pkg.MuzzleDirective

So I just made the type implements Serializable and everything went smooth after.

open class MuzzleDirective : Serializable {
  // fields
}

My question is whether it’s needed to make it so ?

I have an intuition that I didn’t work much yet, is that implementing Serializable is necessary when fields are using “raw” types like Boolean , String, List<String> ; while it might not be necessary when the files use “property wrappers” like Property<String>, SetProperty<String>, etc? But I could be completely wrong since I didn’t tested that.

Depends on what you do exactly.
What you showed should work fine:

open class MuzzleDirective {
    var additionalDeps: MutableList<String> = ArrayList()
}

abstract class Foo : DefaultTask() {
    @get:Input
    @get:Optional
    abstract val muzzleDirective: Property<MuzzleDirective>

    @TaskAction
    fun execute() {
        muzzleDirective.get().additionalDeps.forEach { println("foo: $it") }
    }
}

val foo by tasks.registering(Foo::class) {
    muzzleDirective = objects.newInstance<MuzzleDirective>().apply {
        additionalDeps.add("foo")
    }
}

But usually it is better to have Property and friends everywhere, so that consumers can lazily wire values in and also preserve task dependencies.

And you would usually have input annotations on the properties of the nested elements themselves and use @Nested where you use it.

It’s not exactly that the MuzzleDirective is an object configured via the extension method pass.

So projects already declare

muzzle {
  pass {
    // pass directive 1
  }
  pass {
    // pass directive 2
  }
  pass {
    // pass directive 3
  }
  fail { directive 3 }
} 

Without implementing Serializable this is definitively failing. I’ll try with the properties (& nested) instead to see if that changes.

I just condensed the example and left out irrelevant part as of what you desribed.
This as well works without any problem:

abstract class MuzzleExtension: ExtensionAware {
    @get:Inject
    abstract val objectFactory: ObjectFactory

    abstract val directives: ListProperty<MuzzleDirective>

    fun pass(action: Action<in MuzzleDirective>) {
        val pass = objectFactory.newInstance<MuzzleDirective>()
        action.execute(pass)
        pass.assertPass = true
        directives.add(pass)
    }
}

open class MuzzleDirective {
    var additionalDeps: MutableList<String> = ArrayList()
    var assertPass: Boolean = false
}

abstract class Foo : DefaultTask() {
    @get:Input
    @get:Optional
    abstract val muzzleDirective: Property<MuzzleDirective>

    @TaskAction
    fun execute() {
        muzzleDirective.get().additionalDeps.forEach { println("foo: $it") }
    }
}

val muzzle = extensions.create<MuzzleExtension>("muzzle")

configure<MuzzleExtension> {
    pass {
        additionalDeps.add("foo")
    }
}

val foo by tasks.registering(Foo::class) {
    muzzleDirective = muzzle.directives.get().first()
}

So you really need to knit an MCVE showing what you do exactly to clarify what the problem might be.

Thanks for that :slight_smile:

I would like to create an MVCE, but it’s a bit of a big project, big plugin. Right the workaround of making this serializable seems to work. Also, I don’t want to make this consume your time. It’s probably not worth it even though it’d be nice to understand the cause in my setup.

But from the stacktrace the MuzzleDirective is passed to a JDK’s ObjectOutputStream which needs the object to be Serializable. Apparently this mechanism is the fallback, when everything else didn’t match/work in AbstractValueProcessor. Maybe no ValueSnapshotterSerializerRegistry could work ?
Using Gradle 8.14.3, and unsure Gradle 9 change things on this matter.

...
Caused by: org.gradle.internal.execution.InputFingerprinter$InputFingerprintingException: Cannot fingerprint input property 'muzzleDirective': value 'pass io.dropwizard:dropwizard-client:[,3)' cannot be serialized.
        at org.gradle.internal.execution.impl.DefaultInputFingerprinter$InputCollectingVisitor.visitInputProperty(DefaultInputFingerprinter.java:114)
        at org.gradle.api.internal.tasks.execution.TaskExecution.visitRegularInputs(TaskExecution.java:315)
        at org.gradle.internal.execution.impl.DefaultInputFingerprinter.fingerprintInputProperties(DefaultInputFingerprinter.java:63)
...
Caused by: org.gradle.internal.snapshot.ValueSnapshottingException: Could not serialize value of type MuzzleDirective
        at org.gradle.internal.snapshot.impl.AbstractValueProcessor.newValueSerializationException(AbstractValueProcessor.java:223)
        at org.gradle.internal.snapshot.impl.AbstractValueProcessor.javaSerialized(AbstractValueProcessor.java:214)
        at org.gradle.internal.snapshot.impl.AbstractValueProcessor.javaSerialization(AbstractValueProcessor.java:205)
        at org.gradle.internal.snapshot.impl.AbstractValueProcessor.processValue(AbstractValueProcessor.java:119)
        at org.gradle.internal.snapshot.impl.DefaultValueSnapshotter.snapshot(DefaultValueSnapshotter.java:47)
        at org.gradle.internal.execution.impl.DefaultInputFingerprinter$InputCollectingVisitor.visitInputProperty(DefaultInputFingerprinter.java:107)
        ... 103 more
Caused by: java.io.NotSerializableException: datadog.gradle.plugin.muzzle.MuzzleDirective_Decorated
        at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)
        at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)
        at org.gradle.internal.snapshot.impl.AbstractValueProcessor.javaSerialized(AbstractValueProcessor.java:211)
        ... 107 more

Oh, wait, my test was bad.
My task did not have outputs, so the inputs were never fingerprinted as without output the task cannot be up-to-date.
Adding outputs.upToDateWhen { true } makes it fail:

open class MuzzleDirective {
    var additionalDeps: MutableList<String> = ArrayList()
}

abstract class Foo : DefaultTask() {
    @get:Input
    @get:Optional
    abstract val muzzleDirective: Property<MuzzleDirective>

    init {
        outputs.upToDateWhen { true }
    }

    @TaskAction
    fun execute() {
        muzzleDirective.get().additionalDeps.forEach { println("foo: $it") }
    }
}

val foo by tasks.registering(Foo::class) {
    muzzleDirective = objects.newInstance<MuzzleDirective>().apply {
        additionalDeps.add("foo")
    }
}

This on the other hand works:

    abstract val additionalDeps: ListProperty<String>
}

abstract class Foo : DefaultTask() {
    @get:Input
    @get:Optional
    abstract val muzzleDirective: Property<MuzzleDirective>

    init {
        outputs.upToDateWhen { true }
    }

    @TaskAction
    fun execute() {
        muzzleDirective.get().additionalDeps.get().forEach { println("foo: $it") }
    }
}

val foo by tasks.registering(Foo::class) {
    muzzleDirective = objects.newInstance<MuzzleDirective>().apply {
        additionalDeps.add("foo")
    }
}

And yes, if Gradle needs to serialize something and cannot, the last try is Java Serialization.
So you can either make sure Gradle does not need to fall back to Java Serialization, or support Java Serialization.

OK that explains the behavior. This part is not yet migrated to gradle properties and needs to rely on Java serialization. I don’t know if I’ve seen that in the javadoc or userguide, but that is useful to know.

(Seems I forgot to press “Send”)

Btw, here it is documented explicitly: Properties and Providers

A mutable managed property is declared using a getter method of type Property<T> , where T can be any serializable type or a fully managed Gradle type. The property must not have any setter methods.

So either fully-managed, or serializable.

Cool thanks for the link. I totally missed that.