Repository: conan-io/conan
Pull request: Improve error messages when parsing JSON input files
Changelog: Feature: Improve error messages when dealing with incorrect JSON input file formats.
Docs: omit

Close https://github.com/conan-io/conan/issues/17882

- [x] Refer to the issue that supports this Pull Request.
- [x] If the issue has missing info, explain the purpose/use case/pain/need that covers this Pull Request.
- [x] I've read the [Contributing guide](https://github.com/conan-io/conan/blob/develop2/.github/CONTRIBUTING.md).
- [x] I've followed the PEP8 style guides for Python code.
- [ ] I've opened another PR in the Conan docs repo to the ``develop`` branch, documenting this one.


Problem statement:
Improve error messages when provided wrong json
- Package-lists and graph json files are used by users incorrectly as inputs
- The error messages are not that clear, it would be nice if possible to say: "This json file should be a pkg-list, but it seems you provided a file that is not a package list. It seems to be a graph.json file"
- For some commands with package lists it is important the "origin", if it is in the remote for a promotion (this might be for conan-extensions art:promote), so the pkglist must be the one after the upload, maybe the messages can explain this?

Hints from issue discussion:

Commit message: Restore absolute paths in API

Patch to review:
diff --git a/conan/api/model/list.py b/conan/api/model/list.py
index 520d221faa3..7ad45c288df 100644
--- a/conan/api/model/list.py
+++ b/conan/api/model/list.py
@@ -4,6 +4,7 @@
 from json import JSONDecodeError
 
 from conan.api.model import RecipeReference, PkgReference
+from conan.cli import make_abs_path
 from conan.errors import ConanException
 from conan.internal.errors import NotFoundException
 from conan.internal.model.version_range import VersionRange
@@ -48,6 +49,13 @@ def load(file):
             raise ConanException(f"Package list file invalid JSON: {file}\n{e}")
         except Exception as e:
             raise ConanException(f"Package list file missing or broken: {file}\n{e}")
+        if "graph" in content and "command":
+            relative_path = os.path.relpath(file, os.getcwd())
+            raise ConanException(
+                'Expected a package list file but found a graph file. You can create a "package list" JSON file by running:\n\n'
+                f"\tconan list --graph {relative_path} --format=json > pkglist.json\n\n"
+                "More Info at 'https://docs.conan.io/2/examples/commands/pkglists.html"
+            )
         result = {}
         for remote, pkglist in content.items():
             if "error" in pkglist:
@@ -65,22 +73,30 @@ def load_graph(graphfile, graph_recipes=None, graph_binaries=None, context=None)
         try:
             base_context = context.split("-")[0] if context else None
             graph = json.loads(load(graphfile))
-            mpkglist =  MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
-                                                        context=base_context)
-            if context == "build-only":
-                host = MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
-                                                       context="host")
-                mpkglist.keep_outer(host)
-            elif context == "host-only":
-                build = MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
-                                                        context="build")
-                mpkglist.keep_outer(build)
-            return mpkglist
         except JSONDecodeError as e:
             raise ConanException(f"Graph file invalid JSON: {graphfile}\n{e}")
         except Exception as e:
             raise ConanException(f"Graph file broken: {graphfile}\n{e}")
 
+        if "graph" not in graph:
+            raise ConanException(
+                'Expected a graph file but found an unexpected JSON file format. You can create a "graph" JSON file by running:\n\n'
+                f"\tconan [ graph-info | create | export-pkg | install | test ] --format=json > graph.json\n\n"
+                "More Info at 'https://docs.conan.io/2/reference/commands/formatters/graph_info_json_formatter.html"
+            )
+
+        mpkglist =  MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
+                                                    context=base_context)
+        if context == "build-only":
+            host = MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
+                                                   context="host")
+            mpkglist.keep_outer(host)
+        elif context == "host-only":
+            build = MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
+                                                    context="build")
+            mpkglist.keep_outer(build)
+        return mpkglist
+
     @staticmethod
     def _define_graph(graph, graph_recipes=None, graph_binaries=None, context=None):
         pkglist = MultiPackagesList()
diff --git a/test/integration/command/list/list_test.py b/test/integration/command/list/list_test.py
index 32fd9876030..b6790db51e1 100644
--- a/test/integration/command/list/list_test.py
+++ b/test/integration/command/list/list_test.py
@@ -60,6 +60,19 @@ def test_graph_file_error(self):
         c.run("list --graph=graph.json", assert_error=True)
         assert "ERROR: Graph file broken" in c.out
 
+        # This can happen when using a pkg list file instead of a graph file
+        c.save({"conanfile.py": GenConanfile("lib")})
+        c.run("create . --version=1.0 --format=json", redirect_stdout="graph.json")
+        c.run("list --graph graph.json --format=json", redirect_stdout="pkglist.json")
+        c.run("list --graph pkglist.json", assert_error=True)
+        assert (
+            'Expected a graph file but found an unexpected JSON file format. You can create a "graph" JSON file by running'
+            in c.out
+        )
+        assert (
+            "conan [ graph-info | create | export-pkg | install | test ] --format=json > graph.json"
+            in c.out
+        )
 
 @pytest.fixture(scope="module")
 def client():
diff --git a/test/integration/command/list/test_combined_pkglist_flows.py b/test/integration/command/list/test_combined_pkglist_flows.py
index e1e8d281ef8..6f2d00a569c 100644
--- a/test/integration/command/list/test_combined_pkglist_flows.py
+++ b/test/integration/command/list/test_combined_pkglist_flows.py
@@ -268,6 +268,16 @@ def test_pkglist_file_error(self):
         c.run("pkglist merge -l mylist.json", assert_error=True)
         assert "ERROR: Package list file invalid JSON:" in c.out
 
+        # This can happen when using a graph JSON file instead of a package list file
+        c.save({"conanfile.py": GenConanfile("lib")})
+        c.run("create . --version=1.0 --format=json", redirect_stdout="out/graph.json")
+        c.run("pkglist merge -l out/graph.json", assert_error=True)
+        assert (
+            'ERROR: Expected a package list file but found a graph file. You can create a "package list" JSON file by running:'
+            in c.out
+        )
+        # Check correct relative path on message
+        assert "conan list --graph out/graph.json --format=json > pkglist.json" in c.out
 
 class TestDownloadUpload:
     @pytest.fixture()


Code review comments:
diff_hunk: @@ -65,22 +73,30 @@ def load_graph(graphfile, graph_recipes=None, graph_binaries=None, context=None)
         try:
             base_context = context.split("-")[0] if context else None
             graph = json.loads(load(graphfile))
-            mpkglist =  MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
-                                                        context=base_context)
-            if context == "build-only":
-                host = MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
-                                                       context="host")
-                mpkglist.keep_outer(host)
-            elif context == "host-only":
-                build = MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
-                                                        context="build")
-                mpkglist.keep_outer(build)
-            return mpkglist
         except JSONDecodeError as e:
             raise ConanException(f"Graph file invalid JSON: {graphfile}\n{e}")
         except Exception as e:
             raise ConanException(f"Graph file broken: {graphfile}\n{e}")
 
+        if "graph" not in graph:
+            raise ConanException(
+                'Expected a graph file but found an unexpected JSON file format. You can create a "graph" JSON file by running:\n\n'
+                f"\tconan [ graph-info | create | export-pkg | install | test ] --format=json > graph.json\n\n"
+                "More Info at 'https://docs.conan.io/2/reference/commands/formatters/graph_info_json_formatter.html"
+            )
+
+        mpkglist =  MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
+                                                    context=base_context)
+        if context == "build-only":
+            host = MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
+                                                   context="host")
+            mpkglist.keep_outer(host)
+        elif context == "host-only":
+            build = MultiPackagesList._define_graph(graph, graph_recipes, graph_binaries,
+                                                    context="build")
+            mpkglist.keep_outer(build)
path: conan/api/model/list.py
text: @user1:
If there is any other unexpected error in the graph.json, being it valid json, but fails to serialize, this will raise. It should keep the ``except Exception``, so maybe leave it in the original location and just add the new validation.

diff_hunk: @@ -48,6 +49,13 @@ def load(file):
             raise ConanException(f"Package list file invalid JSON: {file}\n{e}")
         except Exception as e:
             raise ConanException(f"Package list file missing or broken: {file}\n{e}")
+        if "graph" in content and "command":
+            relative_path = os.path.relpath(file, os.getcwd())
path: conan/api/model/list.py
text: @user1:
``relpath`` is problematic and break in windows if the absolute path is a different drive, which happens often in CI environments.
Either use just the filename or a generic one