diff --git a/src/Normalizer/Formatter/JsonFormatter.php b/src/Normalizer/Formatter/JsonFormatter.php index 6e46220d..f97b3455 100644 --- a/src/Normalizer/Formatter/JsonFormatter.php +++ b/src/Normalizer/Formatter/JsonFormatter.php @@ -17,6 +17,7 @@ use function is_scalar; use function json_encode; +use const JSON_FORCE_OBJECT; use const JSON_THROW_ON_ERROR; /** @internal */ @@ -45,6 +46,12 @@ public function format(mixed $value): void } elseif ($value instanceof EmptyObject) { $this->write('{}'); } elseif (is_iterable($value)) { + if ($value === [] && $this->jsonEncodingOptions & JSON_FORCE_OBJECT) { + $this->write('{}'); + + return; + + } // Note: when a generator is formatted, it is considered as a list // if its first key is 0. This is done early because the first JSON // character for an array differs from the one for an object, and we diff --git a/src/Normalizer/JsonNormalizer.php b/src/Normalizer/JsonNormalizer.php index 872955ed..3ccb8702 100644 --- a/src/Normalizer/JsonNormalizer.php +++ b/src/Normalizer/JsonNormalizer.php @@ -14,6 +14,7 @@ use function is_resource; use function stream_get_contents; +use const JSON_FORCE_OBJECT; use const JSON_HEX_AMP; use const JSON_HEX_APOS; use const JSON_HEX_QUOT; @@ -34,7 +35,8 @@ */ final class JsonNormalizer implements Normalizer { - private const ACCEPTABLE_JSON_OPTIONS = JSON_HEX_QUOT + private const ACCEPTABLE_JSON_OPTIONS = JSON_FORCE_OBJECT + | JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS diff --git a/tests/Integration/Normalizer/NormalizerTest.php b/tests/Integration/Normalizer/NormalizerTest.php index 80bbb59e..e761ce2d 100644 --- a/tests/Integration/Normalizer/NormalizerTest.php +++ b/tests/Integration/Normalizer/NormalizerTest.php @@ -30,6 +30,7 @@ use function array_merge; +use const JSON_FORCE_OBJECT; use const JSON_HEX_TAG; use const JSON_THROW_ON_ERROR; @@ -194,6 +195,21 @@ public static function normalize_basic_values_yields_expected_output_data_provid 'expected json' => '{"foo":"foo","bar":"bar"}', ]; + yield 'empty stdClass' => [ + 'input' => (fn () => new stdClass())(), + 'expected array' => [], + 'expected json' => '[]', + ]; + + yield 'empty stdClass kept as object in json' => [ + 'input' => (fn () => new stdClass())(), + 'expected array' => [], + 'expected json' => '{}', + [], + [], + JSON_FORCE_OBJECT + ]; + yield 'ArrayObject' => [ 'input' => new ArrayObject(['foo' => 'foo', 'bar' => 'bar']), 'expected array' => [ @@ -1148,16 +1164,13 @@ public function test_json_transformer_will_always_throw_on_error(): void public function test_json_transformer_only_accepts_acceptable_json_options(): void { - $normalizer = $this->mapperBuilder()->normalizer(Format::json())->withOptions(JSON_FORCE_OBJECT); - self::assertSame(JSON_THROW_ON_ERROR, (fn () => $this->jsonEncodingOptions)->call($normalizer)); - $normalizer = $this->mapperBuilder()->normalizer(Format::json())->withOptions(JSON_PARTIAL_OUTPUT_ON_ERROR); self::assertSame(JSON_THROW_ON_ERROR, (fn () => $this->jsonEncodingOptions)->call($normalizer)); $normalizer = $this->mapperBuilder()->normalizer(Format::json())->withOptions(JSON_PRETTY_PRINT); self::assertSame(JSON_THROW_ON_ERROR, (fn () => $this->jsonEncodingOptions)->call($normalizer)); - $normalizer = $this->mapperBuilder()->normalizer(Format::json())->withOptions(JSON_FORCE_OBJECT | JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_PRETTY_PRINT); + $normalizer = $this->mapperBuilder()->normalizer(Format::json())->withOptions(JSON_PARTIAL_OUTPUT_ON_ERROR | JSON_PRETTY_PRINT); self::assertSame(JSON_THROW_ON_ERROR, (fn () => $this->jsonEncodingOptions)->call($normalizer)); } }