Repository: elsa-workflows/elsa-core (2026-06-09T19:28:54Z)

Issue:
Fail clearly when NotFoundActivity is unavailable during activity deserialization
## Summary
`ActivityJsonConverter` still null-forgives the `NotFoundActivity` descriptor lookup in the missing-activity fallback path.

## Evidence
File: `src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs`

The code does:

```csharp
var notFoundActivityDescriptor = activityRegistry.Find<NotFoundActivity>()!;
```

If registry population is incomplete or startup ordering fails, deserialization degrades into a `NullReferenceException` instead of a diagnosable error.

Related context: issue #6804 documents a failure mode in this area.

## Risk
Medium maintainability/diagnostics risk.

## Constraints
- Safe code-only fix only.
- No persisted-data changes.
- Preserve public APIs.

## Suggested fix
Replace the null-forgiving access with an explicit guard and clear exception message that explains the registry state problem.

## Acceptance criteria
- Missing `NotFoundActivity` descriptor produces a clear, intentional exception.
- Existing successful deserialization behavior is unchanged.
- Targeted tests cover the guarded path.


Discussion:
Confirmed against `main` in `ActivityJsonConverter`: the missing-activity fallback still does `activityRegistry.Find<NotFoundActivity>()!`.

I think the solution is straightforward and low-risk: replace the null-forgiving lookup with an explicit guard and throw a clear exception if the `NotFoundActivity` descriptor is unavailable. That keeps the current happy path unchanged while turning a secondary `NullReferenceException` into a diagnosable startup/registry error.



Patch:
diff --git a/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs
index abd313c054..a3f65de738 100644
--- a/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs
+++ b/src/modules/Elsa.Workflows.Core/Serialization/Converters/ActivityJsonConverter.cs
@@ -49,7 +49,11 @@ public override IActivity Read(ref Utf8JsonReader reader, Type typeToConvert, Js
                 // If the activity type is not found, create a NotFoundActivity instead.
                 if (activityDescriptor == null)
                 {
-                    var notFoundActivityDescriptor = activityRegistry.Find<NotFoundActivity>()!;
+                    var notFoundActivityDescriptor = activityRegistry.Find<NotFoundActivity>();
+
+                    if (notFoundActivityDescriptor == null)
+                        throw new InvalidOperationException($"Unable to deserialize activity type '{activityTypeName}' because the NotFoundActivity descriptor is not registered. Ensure the activity registry has been populated before deserializing workflows.");
+
                     var notFoundActivityResult = JsonActivityConstructorContextHelper.CreateActivity<NotFoundActivity>(notFoundActivityDescriptor, activityRoot, clonedOptions);
                     LogExceptionsIfAny(notFoundActivityResult);
 
@@ -186,4 +190,4 @@ private JsonSerializerOptions GetClonedWriterOptions(JsonSerializerOptions optio
         clonedOptions.Converters.Add(new JsonIgnoreCompositeRootConverterFactory(serviceProvider.GetRequiredService<ActivityWriter>()));
         return clonedOptions;
     }
-}
\ No newline at end of file
+}


Test patch:
diff --git a/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs b/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs
index 5d088a57b2..fd6c3d76c4 100644
--- a/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs
+++ b/test/unit/Elsa.Workflows.Core.UnitTests/Serialization/Converters/ActivityJsonConverterTests.cs
@@ -48,11 +48,7 @@ public void When_DeserializeKnownActivity_And_TypeNameSpecified_And_VersionSpeci
     public void When_DeserializeUnknownActivity_Then_ReturnsNotFoundActivity()
     {
         // Arrange
-        var activityRegistry = Substitute.For<IActivityRegistry>();
-        activityRegistry
-            .Find(NotFoundActivityTypeName)
-            .Returns(new ActivityDescriptor());
-
+        var activityRegistry = CreateUnknownActivityRegistry(new ActivityDescriptor());
         var sut = CreateSut(activityRegistry);
 
         // Act
@@ -92,6 +88,20 @@ public void When_DeserializeNestedNotFoundActivity_Then_PreservesMissingActivity
         Assert.True(notFoundActivity.Metadata.ContainsKey("description"));
     }
 
+    [Fact]
+    public void When_DeserializeUnknownActivity_And_NotFoundActivityDescriptorMissing_Then_ThrowsClearException()
+    {
+        // Arrange
+        var activityRegistry = CreateUnknownActivityRegistry();
+        var sut = CreateSut(activityRegistry);
+
+        // Act
+        var exception = Assert.Throws<InvalidOperationException>(() => Execute(sut, UnknownActivityJson));
+
+        // Assert
+        Assert.Equal($"Unable to deserialize activity type '{UnknownActivityTypeName}' because the NotFoundActivity descriptor is not registered. Ensure the activity registry has been populated before deserializing workflows.", exception.Message);
+    }
+
     [Fact]
     public void When_DeserializeWorkflowAsActivity_And_WorkflowDefinitionIdSpecified_Then_FindsAndInstantiatesActivity()
     {
@@ -164,6 +174,13 @@ static IActivityRegistry CreateActivityRegistry(string typeName, IActivity activ
         return activityRegistry;
     }
 
+    static IActivityRegistry CreateUnknownActivityRegistry(ActivityDescriptor? notFoundActivityDescriptor = null)
+    {
+        var activityRegistry = Substitute.For<IActivityRegistry>();
+        activityRegistry.Find(NotFoundActivityTypeName).Returns(notFoundActivityDescriptor);
+        return activityRegistry;
+    }
+
     static IActivityRegistry CreateActivityRegistry_FindByCustomProperty(string customPropertyName, string customPropertyValue)
     {
         var descriptor = new ActivityDescriptor