diff --git a/src/Normalizer/Formatter/JsonFormatter.php b/src/Normalizer/Formatter/JsonFormatter.php index 6e46220d..e7dfe298 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 */ @@ -54,8 +55,11 @@ public function format(mixed $value): void // afterward, this leads to a JSON array being written, while it // should have been an object. This is a trade-off we accept, // considering most generators starting at 0 are actually lists. - $isList = ($value instanceof Generator && $value->key() === 0) - || (is_array($value) && array_is_list($value)); + $isList = ! ($this->jsonEncodingOptions & JSON_FORCE_OBJECT) + && ( + ($value instanceof Generator && $value->key() === 0) + || (is_array($value) && array_is_list($value)) + ); $isFirst = true; 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..6599fd70 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 'list' => [ + 'input' => ['foo', 'bar'], + 'expected array' => ['foo', 'bar'], + 'expected json' => '["foo","bar"]', + ]; + + yield 'list kept as object in json' => [ + 'input' => ['foo', 'bar'], + 'expected array' => ['foo', 'bar'], + 'expected json' => '{"0":"foo","1":"bar"}', + [], + [], + 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)); } }