diff --git a/core/src/util/serde.rs b/core/src/util/serde.rs index f5a4e3526..4c45813be 100644 --- a/core/src/util/serde.rs +++ b/core/src/util/serde.rs @@ -1456,7 +1456,8 @@ pub fn is_partial_of(partial: &Value, full: &Value) -> bool { if let Some(v_full) = full.get(k) { is_partial_of(v, v_full) } else { - false + // null in partial matches a missing key in full (both represent absence) + v.is_null() } }), (Value::Array(partial), Value::Array(full)) => partial diff --git a/sdk/base/lib/actions/index.ts b/sdk/base/lib/actions/index.ts index c4568ab1c..606f5b4d6 100644 --- a/sdk/base/lib/actions/index.ts +++ b/sdk/base/lib/actions/index.ts @@ -74,6 +74,18 @@ const _validate: T.Task = {} as TaskOptions & { severity: T.TaskSeverity } +/** Recursively converts undefined values to null so they survive JSON serialization */ +function undefinedToNull(obj: unknown): unknown { + if (obj === undefined) return null + if (obj === null || typeof obj !== 'object') return obj + if (Array.isArray(obj)) return obj.map(undefinedToNull) + const result: Record = {} + for (const [k, v] of Object.entries(obj)) { + result[k] = undefinedToNull(v) + } + return result +} + export const createTask = >(options: { effects: T.Effects packageId: T.PackageId @@ -83,8 +95,13 @@ export const createTask = >(options: { }) => { const request = options.options || {} const actionId = options.action.id + const input = + 'input' in request && request.input + ? { ...request.input, value: undefinedToNull(request.input.value) } + : (request as any).input const req = { ...request, + input, actionId, packageId: options.packageId, action: undefined,