Repository: conan-io/conan

Issue:
[bug] JSON serialization of CppInfo _Component is broken
### Describe the bug

We only just are updating from conan 2.11 and hit this issue.

As of https://github.com/conan-io/conan/commit/eb95f5b9472f8d75888d0ffcecba32e17a882423 CppInfo _Component _type is being stored as a PackageType instead of a str:
https://github.com/conan-io/conan/blob/ad7f30275329c63c924d3201463a1143ba1a739c/conan/internal/model/cpp_info.py#L298-L300

But the serialize() method just stores it directly https://github.com/conan-io/conan/blob/ad7f30275329c63c924d3201463a1143ba1a739c/conan/internal/model/cpp_info.py#L130

This means later when you format a graph as json (e.g. via conan install -f json), you will get an error trying to JSON serialize PackageType
https://github.com/conan-io/conan/blob/ad7f30275329c63c924d3201463a1143ba1a739c/conan/cli/formatters/graph/graph.py#L50-L52
```
ERROR: Traceback (most recent call last):
  File "pyvenv\Lib\site-packages\conan\cli\cli.py", line 297, in main
    cli.run(args)
    ~~~~~~~^^^^^^
  File "pyvenv\Lib\site-packages\conan\cli\cli.py", line 194, in run
    command.run(self._conan_api, args[0][1:])
    ~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "pyvenv\Lib\site-packages\conan\cli\command.py", line 200, in run
    self._format(parser, info, *args)
    ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^
  File "pyvenv\Lib\site-packages\conan\cli\command.py", line 113, in _format
    formatter(info)
    ~~~~~~~~~^^^^^^
  File "pyvenv\Lib\site-packages\conan\cli\formatters\graph\graph.py", line 52, in format_graph_json
    json_result = json.dumps({"graph": serial}, indent=4)
  File "python\bin\Lib\json\__init__.py", line 242, in dumps
    **kw).encode(obj)
          ~~~~~~^^^^^
  File "python\bin\Lib\json\encoder.py", line 202, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "python\bin\Lib\json\encoder.py", line 263, in iterencode
    return _iterencode(o, 0)
  File "python\bin\Lib\json\encoder.py", line 182, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
                    f'is not JSON serializable')
TypeError: Object of type PackageType is not JSON serializable
when serializing dict item 'type'
when serializing dict item 'root'
when serializing collections.OrderedDict item 'cpp_info'
when serializing dict item '1'
when serializing collections.OrderedDict item 'nodes'
when serializing dict item 'graph'
```

Now you could simply fix that by changing serialize to str it like other serialize methods do:
```
"type": str(self._type), 
```

However we still would have a problem with deserialize not turning it back into a PackageType as it just uses setattr on the _type which skips past the type property setter.
https://github.com/conan-io/conan/blob/ad7f30275329c63c924d3201463a1143ba1a739c/conan/internal/model/cpp_info.py#L136-L141

### How to reproduce it

We hit it when doing a conan install for something containing a zlib dependency where the package_info for that recipe looks like

```
    def package_info(self):
        self.cpp_info.set_property("cmake_find_mode", "both")
        self.cpp_info.set_property("cmake_file_name", "ZLIB")
        self.cpp_info.set_property("cmake_target_name", "ZLIB::ZLIB")
        self.cpp_info.set_property("pkg_config_name", "zlib")

        if self.settings.os == "Windows" and not self._is_mingw:
            libname = "zdll" if self.options.shared else "zlib"
            if self.options.shared:
                self.cpp_info.location = "bin/zlib1.dll"
                self.cpp_info.link_location = "lib/zdll.lib"
                self.cpp_info.type = str(PackageType.SHARED)
        else:
            libname = "z"
        self.cpp_info.libs = [libname]
```

Here you can see the value of `serial["nodes"]["1"]["cpp_info"]["root"]` which contains the PackageType:
```
{'includedirs': ['.conan\\p\\zlibce2b531051340\\p\\include'], 'srcdirs': None, 'libdirs': ['.conan\\p\\zlibce2b531051340\\p\\lib'], 'resdirs': None, 'bindirs': ['.conan\\p\\zlibce2b531051340\\p\\bin'], 'builddirs': None, 'frameworkdirs': None, 'system_libs': None, 'frameworks': None, 'libs': ['zdll'], 'defines': None, 'cflags': None, 'cxxflags': None, 'sharedlinkflags': None, 'exelinkflags': None, 'objects': None, 'sources': None, 'sysroot': None, 'requires': [], 'requires_private': None, 'properties': {'cmake_find_mode': 'both', 'cmake_file_name': 'ZLIB', 'cmake_target_name': 'ZLIB::ZLIB', 'pkg_config_name': 'zlib'}, 'exe': None, 'type': <PackageType.SHARED: 'shared-library'>, 'location': 'bin/zlib1.dll', 'link_location': 'lib/zdll.lib', 'languages': None}
```

Discussion:

Fix (patch):
diff --git a/conan/internal/model/cpp_info.py b/conan/internal/model/cpp_info.py
index 72111e92f08..80419163e71 100644
--- a/conan/internal/model/cpp_info.py
+++ b/conan/internal/model/cpp_info.py
@@ -127,7 +127,7 @@ def serialize(self):
             "requires": self._requires,
             "properties": self._properties,
             "exe": self._exe,  # single exe, incompatible with libs
-            "type": self._type,
+            "type": str(self._type) if self._type else None,
             "location": self._location,
             "link_location": self._link_location,
             "languages": self._languages
@@ -137,7 +137,12 @@ def serialize(self):
     def deserialize(contents):
         result = _Component()
         for field, value in contents.items():
-            setattr(result, f"_{field}", value)
+            if hasattr(result, field):
+                setattr(result, field, value)
+            else:
+                # If there's on setter, use the internal field, e.g, _properties which has
+                # set_property method, but not a setter
+                setattr(result, f"_{field}", value)
         return result
 
     def clone(self):


Test changes:
diff --git a/test/integration/conanfile/test_cpp_info_serialize.py b/test/integration/conanfile/test_cpp_info_serialize.py
index 9905d91f6ca..1ad0630859c 100644
--- a/test/integration/conanfile/test_cpp_info_serialize.py
+++ b/test/integration/conanfile/test_cpp_info_serialize.py
@@ -23,6 +23,10 @@ def package(self):
                 cpp_info.includedirs = ["myinc"]
                 cpp_info.libs = ["mylib", "myother"]
                 cpp_info.libdirs = ["mylibs"]
+                cpp_info.type = "static-library"
+                cpp_info.set_property("myprop", "myvalue")
+                cpp_info.components["comp"].libs = []
+                cpp_info.components["comp"].type = None
                 p = os.path.join(self.package_folder, "cpp_info.json")
                 cpp_info.save(p)
 
@@ -38,3 +42,8 @@ def package_info(self):
     assert cpp_info["includedirs"][0].endswith("myinc")
     assert cpp_info["libdirs"][0].endswith("mylibs")
     assert cpp_info["libs"] == ["mylib", "myother"]
+    assert cpp_info["type"] == "static-library"
+    assert cpp_info["properties"] == {"myprop": "myvalue"}
+
+    comp = graph["graph"]["nodes"]["1"]["cpp_info"]["comp"]
+    assert comp["type"] is None