Repository: python-poetry/poetry

Issue:
'poetry show' could output more easily machine-readable output
### Issue Kind

Brand new capability

### Description

I've got a need to know what version of a dependency is installed and get other information about that dependency in a machine readable format. The output of `poetry show` lists a lot of useful information, but parsing it with tooling is hard.

```
$ poetry show requests
 name         : requests
 version      : 2.32.4
 description  : Python HTTP for Humans.

dependencies
 - certifi >=2017.4.17
 - charset_normalizer >=2,<4
 - idna >=2.5,<4
 - urllib3 >=1.21.1,<3

required by
 - gcsfs requires *
 - google-api-core requires >=2.18.0,<3.0.0
 - google-cloud-bigquery requires >=2.21.0,<3.0.0
 - google-cloud-storage requires >=2.18.0,<3.0.0dev
 - google-genai requires >=2.28.1,<3.0.0
 - kubernetes requires *
 - pyoauthtarget requires >=2.31.0
 - requests-oauthlib requires >=2.0.0
 - requests-toolbelt requires >=2.0.1,<3.0.0
```

To get the version of `requests`, I'd need to use

```shell
poetry show requests | awk '/version/ { print $3 }'
```

**To suit my exact case, an option such as `--version-only` would suffice.**

but there's other information in there that could be useful, too. I'd love for this output to be in JSON format, e.g.

```json
{
  "name": "requests",
  "version": "2.32.4",
  "description": "Python HTTP for Humans.",
  "dependencies": {
    "certifi": ">=2017.4.17",
    "charset_normalizer": ">=2,<4",
    "idna": ">=2.5,<4",
    "urllib3": ">=1.21.1,<3"
  },
  "required_by": {
    "gcsfs": "*",
    "requests-oauthlib": ">=2.0.0",
    "requests-toolbelt": ">=2.0.1,<3.0.0"
  }
}
```

Then I could do[^awk]:

```shell
poetry show requests | jq .version
```

[^awk]: I recognize the irony in wanting JSON instead of using awk but I reckon it's better for the other sections.


### Impact

Having this would enable users to poll poetry itself for information it already presents but doesn't do so in a way that other tooling can easily consume it.

While a lockfile has this information, reading it requires additional tooling and an understanding of the lockfile format.

### Workarounds

With a TOML reader available, I could read poetry.lock, but in my case, I don't, except perhaps using Poetry's venv which would guarantee that tomllib or toml are available.


Discussion:
i'll try my hand at this



Patch:
diff --git a/docs/cli.md b/docs/cli.md
index bc42290defd..458d3e0e79b 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -1061,6 +1061,7 @@ required by
 * `--all (-a)`: Show all packages (even those not compatible with current system).
 * `--top-level (-T)`: Only show explicitly defined packages.
 * `--no-truncate`: Do not truncate the output based on the terminal width.
+* `--format (-f)`: Specify the output format (`json` or `text`). Default is `text`. `json` cannot be combined with the `--tree` option.
 
 {{% note %}}
 When `--only` is specified, `--with` and `--without` options are ignored.
diff --git a/src/poetry/console/commands/show.py b/src/poetry/console/commands/show.py
index e606f388a2c..779f423bf49 100644
--- a/src/poetry/console/commands/show.py
+++ b/src/poetry/console/commands/show.py
@@ -1,7 +1,9 @@
 from __future__ import annotations
 
+import json
 import sys
 
+from enum import Enum
 from typing import TYPE_CHECKING
 from typing import ClassVar
 
@@ -37,6 +39,11 @@ def reverse_deps(pkg: Package, repo: Repository) -> dict[str, str]:
     return required_by
 
 
+class OutputFormats(str, Enum):
+    JSON = "json"
+    TEXT = "text"
+
+
 class ShowCommand(GroupCommand, EnvCommand):
     name = "show"
     description = "Shows information about packages."
@@ -71,6 +78,13 @@ class ShowCommand(GroupCommand, EnvCommand):
             None,
             "Do not truncate the output based on the terminal width.",
         ),
+        option(
+            "format",
+            "f",
+            "Specify the output format (`json` or `text`). Default is `text`. `json` cannot be combined with the <info>--tree</info> option.",
+            flag=False,
+            default="text",
+        ),
     ]
 
     help = """The show command displays detailed information about a package, or
@@ -115,6 +129,19 @@ def handle(self) -> int:
 
                 return 1
 
+        if self.option("format") not in set(OutputFormats):
+            self.line_error(
+                f"<error>Error: Invalid output format. Supported formats are: {', '.join(OutputFormats)}.</error>"
+            )
+
+            return 1
+
+        if self.option("format") != OutputFormats.TEXT and self.option("tree"):
+            self.line_error(
+                "<error>Error: --tree option can only be used with the text output option.</error>"
+            )
+            return 1
+
         if self.option("outdated"):
             self.io.input.set_option("latest", True)
 
@@ -181,6 +208,24 @@ def _display_single_package_information(
 
             return 0
 
+        if self.option("format") == OutputFormats.JSON:
+            package_info: dict[str, str | dict[str, str]] = {
+                "name": pkg.pretty_name,
+                "version": pkg.pretty_version,
+                "description": pkg.description,
+            }
+            if pkg.requires:
+                package_info["dependencies"] = {
+                    dependency.pretty_name: dependency.pretty_constraint
+                    for dependency in pkg.requires
+                }
+            if required_by:
+                package_info["required_by"] = required_by
+
+            self.line(json.dumps(package_info))
+
+            return 0
+
         rows: Rows = [
             ["<info>name</>", f" : <c1>{pkg.pretty_name}</>"],
             ["<info>version</>", f" : <b>{pkg.pretty_version}</b>"],
@@ -236,6 +281,7 @@ def _display_packages_information(
         show_latest = self.option("latest")
         show_all = self.option("all")
         show_top_level = self.option("top-level")
+        show_why = self.option("why")
         width = (
             sys.maxsize
             if self.option("no-truncate")
@@ -245,6 +291,7 @@ def _display_packages_information(
         latest_packages = {}
         latest_statuses = {}
         installed_repo = InstalledRepository.load(self.env)
+        requires = root.all_requires
 
         # Computing widths
         for locked in locked_packages:
@@ -289,7 +336,7 @@ def _display_packages_information(
                         ),
                     )
 
-                    if self.option("why"):
+                    if show_why:
                         required_by = reverse_deps(locked, locked_repository)
                         required_by_length = max(
                             required_by_length,
@@ -306,23 +353,66 @@ def _display_packages_information(
                     ),
                 )
 
-                if self.option("why"):
+                if show_why:
                     required_by = reverse_deps(locked, locked_repository)
                     required_by_length = max(
                         required_by_length, len(" from " + ",".join(required_by.keys()))
                     )
 
+        if self.option("format") == OutputFormats.JSON:
+            packages = []
+
+            for locked in locked_packages:
+                if locked not in required_locked_packages and not show_all:
+                    continue
+
+                if (
+                    show_latest
+                    and self.option("outdated")
+                    and latest_statuses[locked.pretty_name] == "up-to-date"
+                ):
+                    continue
+
+                if show_top_level and not any(locked.satisfies(r) for r in requires):
+                    continue
+
+                package: dict[str, str | list[str]] = {}
+                package["name"] = locked.pretty_name
+                package["installed_status"] = self.get_installed_status(
+                    locked, installed_repo.packages
+                )
+                package["version"] = get_package_version_display_string(
+                    locked, root=self.poetry.file.path.parent
+                )
+
+                if show_latest:
+                    latest = latest_packages[locked.pretty_name]
+                    package["latest_version"] = get_package_version_display_string(
+                        latest, root=self.poetry.file.path.parent
+                    )
+
+                if show_why:
+                    required_by = reverse_deps(locked, locked_repository)
+                    if required_by:
+                        package["required_by"] = list(required_by.keys())
+
+                package["description"] = locked.description
+
+                packages.append(package)
+
+            self.line(json.dumps(packages))
+
+            return 0
+
         write_version = name_length + version_length + 3 <= width
         write_latest = name_length + version_length + latest_length + 3 <= width
 
         why_end_column = (
             name_length + version_length + latest_length + required_by_length
         )
-        write_why = self.option("why") and (why_end_column + 3) <= width
+        write_why = show_why and (why_end_column + 3) <= width
         write_description = (why_end_column + 24) <= width
 
-        requires = root.all_requires
-
         for locked in locked_packages:
             color = "cyan"
             name = locked.pretty_name


Test patch:
diff --git a/tests/console/commands/test_show.py b/tests/console/commands/test_show.py
index 30ebb92f069..0b9558a6ed3 100644
--- a/tests/console/commands/test_show.py
+++ b/tests/console/commands/test_show.py
@@ -1,6 +1,12 @@
 from __future__ import annotations
 
+import json
+
 from typing import TYPE_CHECKING
+from typing import Any
+from typing import Callable
+from typing import TypeVar
+from typing import cast
 
 import pytest
 
@@ -28,8 +34,20 @@ def tester(command_tester_factory: CommandTesterFactory) -> CommandTester:
     return command_tester_factory("show")
 
 
+F = TypeVar("F", bound=Callable[..., Any])
+
+
+def output_format_parametrize(func: F) -> F:
+    formats = ["", "--format json"]
+    return cast("F", pytest.mark.parametrize("output_format", formats)(func))
+
+
+@output_format_parametrize
 def test_show_basic_with_installed_packages(
-    tester: CommandTester, poetry: Poetry, installed: Repository
+    output_format: str,
+    tester: CommandTester,
+    poetry: Poetry,
+    installed: Repository,
 ) -> None:
     poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0"))
     poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0"))
@@ -91,15 +109,38 @@ def test_show_basic_with_installed_packages(
         }
     )
 
-    tester.execute()
+    tester.execute(output_format)
 
-    expected = """\
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = [
+            {
+                "name": "cachy",
+                "version": "0.1.0",
+                "description": "Cachy package",
+                "installed_status": "installed",
+            },
+            {
+                "name": "pendulum",
+                "version": "2.0.0",
+                "description": "Pendulum package",
+                "installed_status": "installed",
+            },
+            {
+                "name": "pytest",
+                "version": "3.7.3",
+                "description": "Pytest package",
+                "installed_status": "installed",
+            },
+        ]
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """\
 cachy    0.1.0 Cachy package
 pendulum 2.0.0 Pendulum package
 pytest   3.7.3 Pytest package
 """
-
-    assert tester.io.fetch_output() == expected
+        assert tester.io.fetch_output() == expected
 
 
 def _configure_project_with_groups(poetry: Poetry, installed: Repository) -> None:
@@ -179,6 +220,23 @@ def _configure_project_with_groups(poetry: Poetry, installed: Repository) -> Non
 pytest 3.7.3 Pytest package
 """,
         ),
+        (
+            "--format json",
+            [
+                {
+                    "name": "cachy",
+                    "version": "0.1.0",
+                    "description": "Cachy package",
+                    "installed_status": "installed",
+                },
+                {
+                    "name": "pytest",
+                    "version": "3.7.3",
+                    "description": "Pytest package",
+                    "installed_status": "installed",
+                },
+            ],
+        ),
         (
             "--with time",
             """\
@@ -187,24 +245,80 @@ def _configure_project_with_groups(poetry: Poetry, installed: Repository) -> Non
 pytest   3.7.3 Pytest package
 """,
         ),
+        (
+            "--with time --format json",
+            [
+                {
+                    "name": "cachy",
+                    "version": "0.1.0",
+                    "description": "Cachy package",
+                    "installed_status": "installed",
+                },
+                {
+                    "name": "pendulum",
+                    "version": "2.0.0",
+                    "description": "Pendulum package",
+                    "installed_status": "installed",
+                },
+                {
+                    "name": "pytest",
+                    "version": "3.7.3",
+                    "description": "Pytest package",
+                    "installed_status": "installed",
+                },
+            ],
+        ),
         (
             "--without test",
             """\
 cachy 0.1.0 Cachy package
 """,
         ),
+        (
+            "--without test --format json",
+            [
+                {
+                    "name": "cachy",
+                    "version": "0.1.0",
+                    "description": "Cachy package",
+                    "installed_status": "installed",
+                },
+            ],
+        ),
         (
             f"--without {MAIN_GROUP}",
             """\
 pytest 3.7.3 Pytest package
 """,
         ),
+        (
+            f"--without {MAIN_GROUP} --format json",
+            [
+                {
+                    "name": "pytest",
+                    "version": "3.7.3",
+                    "description": "Pytest package",
+                    "installed_status": "installed",
+                },
+            ],
+        ),
         (
             f"--only {MAIN_GROUP}",
             """\
 cachy 0.1.0 Cachy package
 """,
         ),
+        (
+            f"--only {MAIN_GROUP} --format json",
+            [
+                {
+                    "name": "cachy",
+                    "version": "0.1.0",
+                    "description": "Cachy package",
+                    "installed_status": "installed",
+                },
+            ],
+        ),
         (
             "--with time --without test",
             """\
@@ -212,24 +326,74 @@ def _configure_project_with_groups(poetry: Poetry, installed: Repository) -> Non
 pendulum 2.0.0 Pendulum package
 """,
         ),
+        (
+            "--with time --without test --format json",
+            [
+                {
+                    "name": "cachy",
+                    "version": "0.1.0",
+                    "description": "Cachy package",
+                    "installed_status": "installed",
+                },
+                {
+                    "name": "pendulum",
+                    "version": "2.0.0",
+                    "description": "Pendulum package",
+                    "installed_status": "installed",
+                },
+            ],
+        ),
         (
             f"--with time --without {MAIN_GROUP},test",
             """\
 pendulum 2.0.0 Pendulum package
 """,
         ),
+        (
+            f"--with time --without {MAIN_GROUP},test --format json",
+            [
+                {
+                    "name": "pendulum",
+                    "version": "2.0.0",
+                    "description": "Pendulum package",
+                    "installed_status": "installed",
+                },
+            ],
+        ),
         (
             "--only time",
             """\
 pendulum 2.0.0 Pendulum package
 """,
         ),
+        (
+            "--only time --format json",
+            [
+                {
+                    "name": "pendulum",
+                    "version": "2.0.0",
+                    "description": "Pendulum package",
+                    "installed_status": "installed",
+                },
+            ],
+        ),
         (
             "--only time --with test",
             """\
 pendulum 2.0.0 Pendulum package
 """,
         ),
+        (
+            "--only time --with test --format json",
+            [
+                {
+                    "name": "pendulum",
+                    "version": "2.0.0",
+                    "description": "Pendulum package",
+                    "installed_status": "installed",
+                },
+            ],
+        ),
         (
             "--with time",
             """\
@@ -238,11 +402,34 @@ def _configure_project_with_groups(poetry: Poetry, installed: Repository) -> Non
 pytest   3.7.3 Pytest package
 """,
         ),
+        (
+            "--with time --format json",
+            [
+                {
+                    "name": "cachy",
+                    "version": "0.1.0",
+                    "description": "Cachy package",
+                    "installed_status": "installed",
+                },
+                {
+                    "name": "pendulum",
+                    "version": "2.0.0",
+                    "description": "Pendulum package",
+                    "installed_status": "installed",
+                },
+                {
+                    "name": "pytest",
+                    "version": "3.7.3",
+                    "description": "Pytest package",
+                    "installed_status": "installed",
+                },
+            ],
+        ),
     ],
 )
 def test_show_basic_with_group_options(
     options: str,
-    expected: str,
+    expected: str | list[dict[str, str]],
     tester: CommandTester,
     poetry: Poetry,
     installed: Repository,
@@ -251,11 +438,18 @@ def test_show_basic_with_group_options(
 
     tester.execute(options)
 
-    assert tester.io.fetch_output() == expected
+    if "json" in options:
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        assert tester.io.fetch_output() == expected
 
 
+@output_format_parametrize
 def test_show_basic_with_installed_packages_single(
-    tester: CommandTester, poetry: Poetry, installed: Repository
+    output_format: str,
+    tester: CommandTester,
+    poetry: Poetry,
+    installed: Repository,
 ) -> None:
     poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0"))
 
@@ -287,17 +481,29 @@ def test_show_basic_with_installed_packages_single(
         }
     )
 
-    tester.execute("cachy")
+    tester.execute(f"cachy {output_format}")
 
-    assert [line.strip() for line in tester.io.fetch_output().splitlines()] == [
-        "name         : cachy",
-        "version      : 0.1.0",
-        "description  : Cachy package",
-    ]
+    expected: dict[str, str] | list[str] = {}
+    if "json" in output_format:
+        expected = {"name": "cachy", "version": "0.1.0", "description": "Cachy package"}
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = [
+            "name         : cachy",
+            "version      : 0.1.0",
+            "description  : Cachy package",
+        ]
+        assert [
+            line.strip() for line in tester.io.fetch_output().splitlines()
+        ] == expected
 
 
+@output_format_parametrize
 def test_show_basic_with_installed_packages_single_canonicalized(
-    tester: CommandTester, poetry: Poetry, installed: Repository
+    output_format: str,
+    tester: CommandTester,
+    poetry: Poetry,
+    installed: Repository,
 ) -> None:
     poetry.package.add_dependency(Factory.create_dependency("foo-bar", "^0.1.0"))
 
@@ -329,17 +535,33 @@ def test_show_basic_with_installed_packages_single_canonicalized(
         }
     )
 
-    tester.execute("Foo_Bar")
+    tester.execute(f"Foo_Bar {output_format}")
 
-    assert [line.strip() for line in tester.io.fetch_output().splitlines()] == [
-        "name         : foo-bar",
-        "version      : 0.1.0",
-        "description  : Foobar package",
-    ]
+    expected: dict[str, str] | list[str] = {}
+    if "json" in output_format:
+        expected = {
+            "name": "foo-bar",
+            "version": "0.1.0",
+            "description": "Foobar package",
+        }
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = [
+            "name         : foo-bar",
+            "version      : 0.1.0",
+            "description  : Foobar package",
+        ]
+        assert [
+            line.strip() for line in tester.io.fetch_output().splitlines()
+        ] == expected
 
 
+@output_format_parametrize
 def test_show_basic_with_not_installed_packages_non_decorated(
-    tester: CommandTester, poetry: Poetry, installed: Repository
+    output_format: str,
+    tester: CommandTester,
+    poetry: Poetry,
+    installed: Repository,
 ) -> None:
     poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.1.0"))
     poetry.package.add_dependency(Factory.create_dependency("pendulum", "^2.0.0"))
@@ -384,14 +606,31 @@ def test_show_basic_with_not_installed_packages_non_decorated(
         }
     )
 
-    tester.execute()
+    tester.execute(output_format)
 
-    expected = """\
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = [
+            {
+                "name": "cachy",
+                "version": "0.1.0",
+                "description": "Cachy package",
+                "installed_status": "installed",
+            },
+            {
+                "name": "pendulum",
+                "version": "2.0.0",
+                "description": "Pendulum package",
+                "installed_status": "not-installed",
+            },
+        ]
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """\
 cachy        0.1.0 Cachy package
 pendulum (!) 2.0.0 Pendulum package
 """
-
-    assert tester.io.fetch_output() == expected
+        assert tester.io.fetch_output() == expected
 
 
 def test_show_basic_with_not_installed_packages_decorated(
@@ -450,7 +689,9 @@ def test_show_basic_with_not_installed_packages_decorated(
     assert tester.io.fetch_output() == expected
 
 
+@output_format_parametrize
 def test_show_latest_non_decorated(
+    output_format: str,
     tester: CommandTester,
     poetry: Poetry,
     installed: Repository,
@@ -509,14 +750,33 @@ def test_show_latest_non_decorated(
         }
     )
 
-    tester.execute("--latest")
+    tester.execute(f"--latest {output_format}")
 
-    expected = """\
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = [
+            {
+                "name": "cachy",
+                "version": "0.1.0",
+                "latest_version": "0.2.0",
+                "description": "Cachy package",
+                "installed_status": "installed",
+            },
+            {
+                "name": "pendulum",
+                "version": "2.0.0",
+                "latest_version": "2.0.1",
+                "description": "Pendulum package",
+                "installed_status": "installed",
+            },
+        ]
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """\
 cachy    0.1.0 0.2.0 Cachy package
 pendulum 2.0.0 2.0.1 Pendulum package
 """
-
-    assert tester.io.fetch_output() == expected
+        assert tester.io.fetch_output() == expected
 
 
 def test_show_latest_decorated(
@@ -590,7 +850,9 @@ def test_show_latest_decorated(
     assert tester.io.fetch_output() == expected
 
 
+@output_format_parametrize
 def test_show_outdated(
+    output_format: str,
     tester: CommandTester,
     poetry: Poetry,
     installed: Repository,
@@ -646,16 +908,30 @@ def test_show_outdated(
         }
     )
 
-    tester.execute("--outdated")
+    tester.execute(f"--outdated {output_format}")
 
-    expected = """\
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = [
+            {
+                "name": "cachy",
+                "version": "0.1.0",
+                "latest_version": "0.2.0",
+                "description": "Cachy package",
+                "installed_status": "installed",
+            },
+        ]
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """\
 cachy 0.1.0 0.2.0 Cachy package
 """
-
-    assert tester.io.fetch_output() == expected
+        assert tester.io.fetch_output() == expected
 
 
+@output_format_parametrize
 def test_show_outdated_with_only_up_to_date_packages(
+    output_format: str,
     tester: CommandTester,
     poetry: Poetry,
     installed: Repository,
@@ -690,14 +966,20 @@ def test_show_outdated_with_only_up_to_date_packages(
         }
     )
 
-    tester.execute("--outdated")
-
-    expected = ""
+    tester.execute(f"--outdated {output_format}")
 
-    assert tester.io.fetch_output() == expected
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = []
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = ""
+        assert tester.io.fetch_output() == expected
 
 
+@output_format_parametrize
 def test_show_outdated_has_prerelease_but_not_allowed(
+    output_format: str,
     tester: CommandTester,
     poetry: Poetry,
     installed: Repository,
@@ -758,16 +1040,30 @@ def test_show_outdated_has_prerelease_but_not_allowed(
         }
     )
 
-    tester.execute("--outdated")
+    tester.execute(f"--outdated {output_format}")
 
-    expected = """\
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = [
+            {
+                "name": "cachy",
+                "version": "0.1.0",
+                "latest_version": "0.2.0",
+                "description": "Cachy package",
+                "installed_status": "installed",
+            },
+        ]
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """\
 cachy 0.1.0 0.2.0 Cachy package
 """
-
-    assert tester.io.fetch_output() == expected
+        assert tester.io.fetch_output() == expected
 
 
+@output_format_parametrize
 def test_show_outdated_has_prerelease_and_allowed(
+    output_format: str,
     tester: CommandTester,
     poetry: Poetry,
     installed: Repository,
@@ -832,16 +1128,30 @@ def test_show_outdated_has_prerelease_and_allowed(
         }
     )
 
-    tester.execute("--outdated")
+    tester.execute(f"--outdated {output_format}")
 
-    expected = """\
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = [
+            {
+                "name": "cachy",
+                "version": "0.1.0.dev1",
+                "latest_version": "0.3.0.dev123",
+                "description": "Cachy package",
+                "installed_status": "installed",
+            },
+        ]
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """\
 cachy 0.1.0.dev1 0.3.0.dev123 Cachy package
 """
-
-    assert tester.io.fetch_output() == expected
+        assert tester.io.fetch_output() == expected
 
 
+@output_format_parametrize
 def test_show_outdated_formatting(
+    output_format: str,
     tester: CommandTester,
     poetry: Poetry,
     installed: Repository,
@@ -900,14 +1210,33 @@ def test_show_outdated_formatting(
         }
     )
 
-    tester.execute("--outdated")
+    tester.execute(f"--outdated {output_format}")
 
-    expected = """\
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = [
+            {
+                "name": "cachy",
+                "version": "0.1.0",
+                "latest_version": "0.2.0",
+                "description": "Cachy package",
+                "installed_status": "installed",
+            },
+            {
+                "name": "pendulum",
+                "version": "2.0.0",
+                "latest_version": "2.0.1",
+                "description": "Pendulum package",
+                "installed_status": "installed",
+            },
+        ]
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """\
 cachy    0.1.0 0.2.0 Cachy package
 pendulum 2.0.0 2.0.1 Pendulum package
 """
-
-    assert tester.io.fetch_output() == expected
+        assert tester.io.fetch_output() == expected
 
 
 @pytest.mark.parametrize(
@@ -919,7 +1248,9 @@ def test_show_outdated_formatting(
         ),
     ],
 )
+@output_format_parametrize
 def test_show_outdated_local_dependencies(
+    output_format: str,
     tester: CommandTester,
     poetry: Poetry,
     installed: Repository,
@@ -1019,20 +1350,42 @@ def test_show_outdated_local_dependencies(
         }
     )
 
-    tester.execute("--outdated")
+    tester.execute(f"--outdated {output_format}")
 
-    expected = """\
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = [
+            {
+                "name": "cachy",
+                "version": "0.2.0",
+                "latest_version": "0.3.0",
+                "description": "Cachy package",
+                "installed_status": "installed",
+            },
+            {
+                "name": "project-with-setup",
+                "version": "0.1.1 ../project_with_setup",
+                "latest_version": "0.1.2 ../project_with_setup",
+                "description": "Demo project.",
+                "installed_status": "installed",
+            },
+        ]
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """\
 cachy              0.2.0                       0.3.0
 project-with-setup 0.1.1 ../project_with_setup 0.1.2 ../project_with_setup
 """
-    assert (
-        "\n".join(line.rstrip() for line in tester.io.fetch_output().splitlines())
-        == expected.rstrip()
-    )
+        assert (
+            "\n".join(line.rstrip() for line in tester.io.fetch_output().splitlines())
+            == expected.rstrip()
+        )
 
 
 @pytest.mark.parametrize("project_directory", ["project_with_git_dev_dependency"])
+@output_format_parametrize
 def test_show_outdated_git_dev_dependency(
+    output_format: str,
     tester: CommandTester,
     poetry: Poetry,
     installed: Repository,
@@ -1118,18 +1471,39 @@ def test_show_outdated_git_dev_dependency(
         }
     )
 
-    tester.execute("--outdated")
+    tester.execute(f"--outdated {output_format}")
 
-    expected = """\
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = [
+            {
+                "name": "cachy",
+                "version": "0.1.0",
+                "latest_version": "0.2.0",
+                "description": "Cachy package",
+                "installed_status": "installed",
+            },
+            {
+                "name": "demo",
+                "version": "0.1.1 9cf87a2",
+                "latest_version": "0.1.2 9cf87a2",
+                "description": "Demo package",
+                "installed_status": "installed",
+            },
+        ]
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """\
 cachy 0.1.0         0.2.0         Cachy package
 demo  0.1.1 9cf87a2 0.1.2 9cf87a2 Demo package
 """
-
-    assert tester.io.fetch_output() == expected
+        assert tester.io.fetch_output() == expected
 
 
 @pytest.mark.parametrize("project_directory", ["project_with_git_dev_dependency"])
+@output_format_parametrize
 def test_show_outdated_no_dev_git_dev_dependency(
+    output_format: str,
     tester: CommandTester,
     poetry: Poetry,
     installed: Repository,
@@ -1214,16 +1588,30 @@ def test_show_outdated_no_dev_git_dev_dependency(
         }
     )
 
-    tester.execute("--outdated --without dev")
+    tester.execute(f"--outdated --without dev {output_format}")
 
-    expected = """\
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = [
+            {
+                "name": "cachy",
+                "version": "0.1.0",
+                "latest_version": "0.2.0",
+                "description": "Cachy package",
+                "installed_status": "installed",
+            },
+        ]
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """\
 cachy 0.1.0 0.2.0 Cachy package
 """
-
-    assert tester.io.fetch_output() == expected
+        assert tester.io.fetch_output() == expected
 
 
+@output_format_parametrize
 def test_show_hides_incompatible_package(
+    output_format: str,
     tester: CommandTester,
     poetry: Poetry,
     installed: Repository,
@@ -1274,16 +1662,29 @@ def test_show_hides_incompatible_package(
         }
     )
 
-    tester.execute()
+    tester.execute(output_format)
 
-    expected = """\
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = [
+            {
+                "name": "pendulum",
+                "version": "2.0.0",
+                "description": "Pendulum package",
+                "installed_status": "installed",
+            },
+        ]
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """\
 pendulum 2.0.0 Pendulum package
 """
-
-    assert tester.io.fetch_output() == expected
+        assert tester.io.fetch_output() == expected
 
 
+@output_format_parametrize
 def test_show_all_shows_incompatible_package(
+    output_format: str,
     tester: CommandTester,
     poetry: Poetry,
     installed: Repository,
@@ -1330,17 +1731,36 @@ def test_show_all_shows_incompatible_package(
         }
     )
 
-    tester.execute("--all")
+    tester.execute(f"--all {output_format}")
 
-    expected = """\
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = [
+            {
+                "name": "cachy",
+                "version": "0.1.0",
+                "description": "Cachy package",
+                "installed_status": "not-installed",
+            },
+            {
+                "name": "pendulum",
+                "version": "2.0.0",
+                "description": "Pendulum package",
+                "installed_status": "installed",
+            },
+        ]
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """\
 cachy     0.1.0 Cachy package
 pendulum  2.0.0 Pendulum package
 """
-
-    assert tester.io.fetch_output() == expected
+        assert tester.io.fetch_output() == expected
 
 
+@output_format_parametrize
 def test_show_hides_incompatible_package_with_duplicate(
+    output_format: str,
     tester: CommandTester,
     poetry: Poetry,
     installed: Repository,
@@ -1380,16 +1800,29 @@ def test_show_hides_incompatible_package_with_duplicate(
         }
     )
 
-    tester.execute()
+    tester.execute(output_format)
 
-    expected = """\
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = [
+            {
+                "name": "cachy",
+                "version": "0.1.1",
+                "description": "Cachy package",
+                "installed_status": "not-installed",
+            }
+        ]
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """\
 cachy (!) 0.1.1 Cachy package
 """
-
-    assert tester.io.fetch_output() == expected
+        assert tester.io.fetch_output() == expected
 
 
+@output_format_parametrize
 def test_show_all_shows_all_duplicates(
+    output_format: str,
     tester: CommandTester,
     poetry: Poetry,
     installed: Repository,
@@ -1429,14 +1862,31 @@ def test_show_all_shows_all_duplicates(
         }
     )
 
-    tester.execute("--all")
+    tester.execute(f"--all {output_format}")
 
-    expected = """\
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = [
+            {
+                "name": "cachy",
+                "version": "0.1.0",
+                "description": "Cachy package",
+                "installed_status": "not-installed",
+            },
+            {
+                "name": "cachy",
+                "version": "0.1.1",
+                "description": "Cachy package",
+                "installed_status": "not-installed",
+            },
+        ]
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """\
 cachy     0.1.0 Cachy package
 cachy (!) 0.1.1 Cachy package
 """
-
-    assert tester.io.fetch_output() == expected
+        assert tester.io.fetch_output() == expected
 
 
 def test_show_non_dev_with_basic_installed_packages(
@@ -1857,7 +2307,7 @@ def test_show_tree_why(
     a.add_dependency(Factory.create_dependency("b", "=0.0.1"))
 
     b = get_package("b", "0.0.1")
-    a.add_dependency(Factory.create_dependency("c", "=0.0.1"))
+    b.add_dependency(Factory.create_dependency("c", "=0.0.1"))
     installed.add_package(b)
 
     c = get_package("c", "0.0.1")
@@ -1906,8 +2356,106 @@ def test_show_tree_why(
     assert tester.io.fetch_output() == expected
 
 
+@output_format_parametrize
+def test_show_why(
+    output_format: str, tester: CommandTester, poetry: Poetry, installed: Repository
+) -> None:
+    poetry.package.add_dependency(Factory.create_dependency("a", "=0.0.1"))
+
+    a = get_package("a", "0.0.1")
+    a.description = "Package A"
+    a.add_dependency(Factory.create_dependency("b", "=0.0.1"))
+    a.add_dependency(Factory.create_dependency("c", "=0.0.1"))
+    installed.add_package(a)
+
+    b = get_package("b", "0.0.1")
+    b.description = "Package B"
+    b.add_dependency(Factory.create_dependency("c", "=0.0.1"))
+    installed.add_package(b)
+
+    c = get_package("c", "0.0.1")
+    c.description = "Package C"
+    installed.add_package(c)
+
+    assert isinstance(poetry.locker, TestLocker)
+    poetry.locker.mock_lock_data(
+        {
+            "package": [
+                {
+                    "name": "a",
+                    "version": "0.0.1",
+                    "description": "Package A",
+                    "dependencies": {"b": "=0.0.1", "c": "=0.0.1"},
+                    "python-versions": "*",
+                    "optional": False,
+                },
+                {
+                    "name": "b",
+                    "version": "0.0.1",
+                    "description": "Package B",
+                    "dependencies": {"c": "=0.0.1"},
+                    "python-versions": "*",
+                    "optional": False,
+                },
+                {
+                    "name": "c",
+                    "version": "0.0.1",
+                    "description": "Package C",
+                    "python-versions": "*",
+                    "optional": False,
+                },
+            ],
+            "metadata": {
+                "python-versions": "*",
+                "platform": "*",
+                "content-hash": "123456789",
+                "files": {"a": [], "b": [], "c": []},
+            },
+        }
+    )
+
+    tester.execute(f"--why {output_format}")
+
+    expected: str | list[dict[str, str | list[str]]] = ""
+    if "json" in output_format:
+        expected = [
+            {
+                "name": "a",
+                "version": "0.0.1",
+                "description": "Package A",
+                "installed_status": "installed",
+            },
+            {
+                "name": "b",
+                "version": "0.0.1",
+                "description": "Package B",
+                "installed_status": "installed",
+                "required_by": ["a"],
+            },
+            {
+                "name": "c",
+                "version": "0.0.1",
+                "description": "Package C",
+                "installed_status": "installed",
+                "required_by": ["a", "b"],
+            },
+        ]
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """\
+a 0.0.1          Package A
+b 0.0.1 from a   Package B
+c 0.0.1 from a,b Package C
+"""
+        assert tester.io.fetch_output() == expected
+
+
+@output_format_parametrize
 def test_show_required_by_deps(
-    tester: CommandTester, poetry: Poetry, installed: Repository
+    output_format: str,
+    tester: CommandTester,
+    poetry: Poetry,
+    installed: Repository,
 ) -> None:
     poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.2.0"))
     poetry.package.add_dependency(Factory.create_dependency("pendulum", "2.0.0"))
@@ -1964,9 +2512,20 @@ def test_show_required_by_deps(
         }
     )
 
-    tester.execute("cachy")
+    tester.execute(f"cachy {output_format}")
 
-    expected = """\
+    expected: str | dict[str, str | dict[str, str]] = ""
+    if "json" in output_format:
+        expected = {
+            "name": "cachy",
+            "version": "0.2.0",
+            "description": "",
+            "dependencies": {"msgpack-python": ">=0.5 <0.6"},
+            "required_by": {"pendulum": ">=0.2.0 <0.3.0"},
+        }
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """\
  name         : cachy
  version      : 0.2.0
  description  :
@@ -1976,9 +2535,9 @@ def test_show_required_by_deps(
 
 required by
  - pendulum requires >=0.2.0 <0.3.0
-""".splitlines()
-    actual = [line.rstrip() for line in tester.io.fetch_output().splitlines()]
-    assert actual == expected
+"""
+        actual = [line.rstrip() for line in tester.io.fetch_output().splitlines()]
+        assert actual == expected.splitlines()
 
 
 @pytest.mark.parametrize("truncate", [False, True])
@@ -2051,7 +2610,9 @@ def test_show_errors_without_lock_file(tester: CommandTester, poetry: Poetry) ->
     assert tester.status_code == 1
 
 
+@output_format_parametrize
 def test_show_dependency_installed_from_git_in_dev(
+    output_format: str,
     tester: CommandTester,
     poetry: Poetry,
     installed: Repository,
@@ -2114,11 +2675,19 @@ def test_show_dependency_installed_from_git_in_dev(
 
     # Nothing needs updating, there is no confusion between the git and not-git
     # packages.
-    tester.execute("--outdated")
-    assert tester.io.fetch_output() == ""
+    tester.execute(f"--outdated {output_format}")
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = []
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = ""
+        assert tester.io.fetch_output() == expected
 
 
+@output_format_parametrize
 def test_url_dependency_is_not_outdated_by_repository_package(
+    output_format: str,
     tester: CommandTester,
     poetry: Poetry,
     installed: Repository,
@@ -2167,12 +2736,23 @@ def test_url_dependency_is_not_outdated_by_repository_package(
 
     # The url dependency on demo is not made outdated by the existence of a newer
     # version in the repository.
-    tester.execute("--outdated")
-    assert tester.io.fetch_output() == ""
+    tester.execute(f"--outdated {output_format}")
+
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = []
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = ""
+        assert tester.io.fetch_output() == expected
 
 
+@output_format_parametrize
 def test_show_top_level(
-    tester: CommandTester, poetry: Poetry, installed: Repository
+    output_format: str,
+    tester: CommandTester,
+    poetry: Poetry,
+    installed: Repository,
 ) -> None:
     poetry.package.add_dependency(Factory.create_dependency("cachy", "^0.2.0"))
 
@@ -2216,15 +2796,30 @@ def test_show_top_level(
         }
     )
 
-    tester.execute("--top-level")
+    tester.execute(f"--top-level {output_format}")
 
-    expected = """cachy              0.2.0 \n"""
-
-    assert tester.io.fetch_output() == expected
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = [
+            {
+                "name": "cachy",
+                "version": "0.2.0",
+                "description": "",
+                "installed_status": "installed",
+            },
+        ]
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """cachy              0.2.0 \n"""
+        assert tester.io.fetch_output() == expected
 
 
+@output_format_parametrize
 def test_show_top_level_with_explicitly_defined_dependency(
-    tester: CommandTester, poetry: Poetry, installed: Repository
+    output_format: str,
+    tester: CommandTester,
+    poetry: Poetry,
+    installed: Repository,
 ) -> None:
     poetry.package.add_dependency(Factory.create_dependency("a", "^0.1.0"))
     poetry.package.add_dependency(Factory.create_dependency("b", "^0.2.0"))
@@ -2271,15 +2866,36 @@ def test_show_top_level_with_explicitly_defined_dependency(
         }
     )
 
-    tester.execute("--top-level")
+    tester.execute(f"--top-level {output_format}")
 
-    expected = """a 0.1.0 \nb 0.2.0 \n"""
-
-    assert tester.io.fetch_output() == expected
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = [
+            {
+                "name": "a",
+                "version": "0.1.0",
+                "description": "",
+                "installed_status": "installed",
+            },
+            {
+                "name": "b",
+                "version": "0.2.0",
+                "description": "",
+                "installed_status": "installed",
+            },
+        ]
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """a 0.1.0 \nb 0.2.0 \n"""
+        assert tester.io.fetch_output() == expected
 
 
+@output_format_parametrize
 def test_show_top_level_with_extras(
-    tester: CommandTester, poetry: Poetry, installed: Repository
+    output_format: str,
+    tester: CommandTester,
+    poetry: Poetry,
+    installed: Repository,
 ) -> None:
     black_dep = Factory.create_dependency(
         "black", {"version": "23.3.0", "extras": ["d"]}
@@ -2340,11 +2956,22 @@ def test_show_top_level_with_extras(
         }
     )
 
-    tester.execute("--top-level")
+    tester.execute(f"--top-level {output_format}")
 
-    expected = """black 23.3.0 \n"""
-
-    assert tester.io.fetch_output() == expected
+    expected: str | list[dict[str, str]] = ""
+    if "json" in output_format:
+        expected = [
+            {
+                "name": "black",
+                "version": "23.3.0",
+                "description": "",
+                "installed_status": "installed",
+            },
+        ]
+        assert json.loads(tester.io.fetch_output()) == expected
+    else:
+        expected = """black 23.3.0 \n"""
+        assert tester.io.fetch_output() == expected
 
 
 def test_show_error_top_level_with_tree(tester: CommandTester) -> None:
@@ -2391,3 +3018,21 @@ def test_show_outdated_missing_directory_dependency(
 
     with pytest.raises(ValueError, match="does not exist"):
         tester.execute("")
+
+
+def test_show_error_invalid_output_format(
+    tester: CommandTester,
+) -> None:
+    expected = "Error: Invalid output format. Supported formats are: json, text.\n"
+    tester.execute("--format invalid")
+    assert tester.io.fetch_error() == expected
+    assert tester.status_code == 1
+
+
+def test_show_error_invalid_output_format_with_tree_option(
+    tester: CommandTester,
+) -> None:
+    expected = "Error: --tree option can only be used with the text output option.\n"
+    tester.execute("--format json --tree")
+    assert tester.io.fetch_error() == expected
+    assert tester.status_code == 1