Missing, null, and zero: updating API fields without erasing good data

An inventory API sends {"stock":0}. That is a valid stock count. Another response omits stock because the supplier did not include that field. Treating both responses as "empty" makes a synchronization job unreliable.
Write the update contract first
For this example, missing means leave the previous value alone. Null means the supplier cannot provide a value, so also preserve the previous count. An integer zero means replace the count with zero. Negative values and strings are invalid.
<?php
function nextStock(array $payload, int $previous): int
{
if (!array_key_exists('stock', $payload)
|| $payload['stock'] === null) {
return $previous;
}
if (!is_int($payload['stock']) || $payload['stock'] < 0) {
throw new InvalidArgumentException('Invalid stock count');
}
return $payload['stock'];
}
assert(nextStock([], 12) === 12);
assert(nextStock(['stock' => null], 12) === 12);
assert(nextStock(['stock' => 0], 12) === 0);
assert(nextStock(['stock' => 7], 12) === 7);
echo "four update cases passed", PHP_EOL;
The explicit type check is intentional. If the supplier documents numeric strings, introduce a separate conversion step with range checks. Do not accept arbitrary coercion just because a few fixtures happen to work.
Why empty is the wrong question
The business question is whether a field is present and valid. A truthiness check answers something else. In the zero-stock case it can preserve twelve units that the supplier says no longer exist. That can flow into an incorrect availability display.
For a nullable profile biography, your contract may instead define null as an intentional deletion. Reuse the decision process, not this stock-specific behavior.
Add tests for negative integers, numeric strings, arrays, and very large numbers. Record invalid responses without storing secrets or the whole upstream payload. The PHP array_key_exists reference explains its distinction from isset for null values.