Repository: conan-io/conan

Issue:
[bug] ERROR: Object of type _PackageOption is not JSON serializable
### Environment details

* Conan version: 2.0.12 (also happens with 2.0.11 and 2.0.10, as far as I tested)
* Python version: 3.11




### Steps to reproduce

Executing `conan install --requires my_package/version@company/channel --build missing --format=json` fails with the following stack trace:

```
ERROR: Traceback (most recent call last):
  File "/Users/dodo/Desktop/conan2-venv/lib/python3.11/site-packages/conan/cli/cli.py", line 272, in main
    cli.run(args)
  File "/Users/dodo/Desktop/conan2-venv/lib/python3.11/site-packages/conan/cli/cli.py", line 172, in run
    command.run(self._conan_api, args[0][1:])
  File "/Users/dodo/Desktop/conan2-venv/lib/python3.11/site-packages/conan/cli/command.py", line 128, in run
    self._format(parser, info, *args)
  File "/Users/dodo/Desktop/conan2-venv/lib/python3.11/site-packages/conan/cli/command.py", line 94, in _format
    formatter(info)
  File "/Users/dodo/Desktop/conan2-venv/lib/python3.11/site-packages/conan/cli/formatters/graph/graph.py", line 143, in format_graph_json
    json_result = json.dumps({"graph": serial}, indent=4)
                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.5/Frameworks/Python.framework/Versions/3.11/lib/python3.11/json/__init__.py", line 238, in dumps
    **kw).encode(obj)
          ^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.5/Frameworks/Python.framework/Versions/3.11/lib/python3.11/json/encoder.py", line 202, in encode
    chunks = list(chunks)
             ^^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.5/Frameworks/Python.framework/Versions/3.11/lib/python3.11/json/encoder.py", line 432, in _iterencode
    yield from _iterencode_dict(o, _current_indent_level)
  File "/opt/homebrew/Cellar/python@3.11/3.11.5/Frameworks/Python.framework/Versions/3.11/lib/python3.11/json/encoder.py", line 406, in _iterencode_dict
    yield from chunks
  File "/opt/homebrew/Cellar/python@3.11/3.11.5/Frameworks/Python.framework/Versions/3.11/lib/python3.11/json/encoder.py", line 406, in _iterencode_dict
    yield from chunks
  File "/opt/homebrew/Cellar/python@3.11/3.11.5/Frameworks/Python.framework/Versions/3.11/lib/python3.11/json/encoder.py", line 406, in _iterencode_dict
    yield from chunks
  [Previous line repeated 3 more times]
  File "/opt/homebrew/Cellar/python@3.11/3.11.5/Frameworks/Python.framework/Versions/3.11/lib/python3.11/json/encoder.py", line 439, in _iterencode
    o = _default(o)
        ^^^^^^^^^^^
  File "/opt/homebrew/Cellar/python@3.11/3.11.5/Frameworks/Python.framework/Versions/3.11/lib/python3.11/json/encoder.py", line 180, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '
TypeError: Object of type _PackageOption is not JSON serializable

ERROR: Object of type _PackageOption is not JSON serializable
```

It didn't happen before with other packages. I'm still trying to figure out what is specific to this package. However, I hope the stack trace can help finding and fixing the issue.

### Logs

_No response_


Discussion:
Hi @DoDoENT 

Thanks for reporting

This seems related to something in the recipe, adding ``self.options.myoption`` directly to some field that didn't expect it. It would be good to see your ``package_id()`` and ``package_info()`` methods, that could give some hints.
OK, after removing this from `package_info`, the error goes away:

```python
        self.conf_info.update(
            'user.microblink.cmaketoolchain:cache_variables',
            {
                'RecognizerUtils_BINARIZATION': self.options.binarization,
                'RecognizerUtils_JSONIZATION': self.options.jsonization,
            }
        )
```

But why?
That makes sense. The ``self.options.xxxx`` is not a primitive type. It is an object of type ``_PackageOption`` that can perform promotions, validation, etc. As internally options are strings (cmake-like), for serialization purposes, and also for ``conf_info`` purposes, they should be casted to the desired type, like ``int(self.options.myoption)`` or ``str(self.options.myoption)`` (as ``conf`` items should be also primitive and Python (lists, dicts) types.
OK, replacing this with 

```python
        self.conf_info.update(
            'user.microblink.cmaketoolchain:cache_variables',
            {
                'RecognizerUtils_BINARIZATION': str(self.options.binarization),
                'RecognizerUtils_JSONIZATION': str(self.options.jsonization),
            }
        )
```

makes the error go away, with no change in behavior.
I understand the error now, but I think that either conan should simply stringify the `_PackageOption` in this case, or throw a more useful error, possibly even validate this (this happened only on my CI machine that uses --format=json). This crash with stack trace looks like a bug in conan, even though the issue is in the recipe.
Implicit stringifying or casting sounds potentially problematic and confusing. For example the stringfication will create ``"None"`` strings if not managed correctly, not processed types like lists could create a weird string if forced to stringify.

I agree that at least the error trace could be captured and tried to raise a nicer error message, I am not sure if the ``conf_info`` should be analyzed and extra serialization should be performed. Note that there are many other objects like ``self.settings.compiler``, that can be used as inputs and that won't serialize automatically, covering every possible corner case robustly seems too much.



Patch:
diff --git a/conan/internal/model/conf.py b/conan/internal/model/conf.py
index 3e3e9595904..4cb9d7c293a 100644
--- a/conan/internal/model/conf.py
+++ b/conan/internal/model/conf.py
@@ -7,7 +7,9 @@
 from collections import OrderedDict
 
 from conan.errors import ConanException
+from conan.internal.model.options import _PackageOption
 from conan.internal.model.recipe_ref import ref_matches
+from conan.internal.model.settings import SettingsItem
 
 BUILT_IN_CONFS = {
     "core:required_conan_version": "Raise if current version does not match the defined range.",
@@ -155,7 +157,7 @@ class _ConfVarPlaceHolder:
     pass
 
 
-class _ConfValue(object):
+class _ConfValue:
 
     def __init__(self, name, value, path=False, update=None):
         if name != name.lower():
@@ -163,6 +165,8 @@ def __init__(self, name, value, path=False, update=None):
         self._name = name
         self._value = value
         self._value_type = type(value)
+        if isinstance(value, (_PackageOption, SettingsItem)):
+            raise ConanException(f"Invalid 'conf' type, please use Python types (int, str, ...)")
         self._path = path
         self._update = update
 
@@ -220,6 +224,8 @@ def append(self, value):
         if isinstance(value, list):
             self._value.extend(value)
         else:
+            if isinstance(value, (_PackageOption, SettingsItem)):
+                raise ConanException(f"Invalid 'conf' type, please use Python types (int, str, ...)")
             self._value.append(value)
 
     def prepend(self, value):
@@ -229,6 +235,8 @@ def prepend(self, value):
         if isinstance(value, list):
             self._value = value + self._value
         else:
+            if isinstance(value, (_PackageOption, SettingsItem)):
+                raise ConanException(f"Invalid 'conf' type, please use Python types (int, str, ...)")
             self._value.insert(0, value)
 
     def compose_conf_value(self, other):


Test patch:
diff --git a/test/integration/configuration/conf/test_conf_from_br.py b/test/integration/configuration/conf/test_conf_from_br.py
index 4b9aafe9ecd..a0d067526e7 100644
--- a/test/integration/configuration/conf/test_conf_from_br.py
+++ b/test/integration/configuration/conf/test_conf_from_br.py
@@ -1,5 +1,7 @@
 import textwrap
 
+import pytest
+
 from conan.test.assets.genconanfile import GenConanfile
 from conan.test.utils.tools import TestClient
 
@@ -266,3 +268,28 @@ def generate(self):
     client.run("install app --build=missing -s:h build_type=Debug -s:b build_type=Release")
     assert "myprotobuf/0.1: MYCONF build: Release" in client.out
     assert "myprotobuf/0.1: MYCONF host: Debug" in client.out
+
+
+@pytest.mark.parametrize("conf", ['define("user.myorg:myconf", self.options.myopt)',
+                                  'define("user.myorg:myconf", self.settings.os)',
+                                  'append("user.myorg:myconf", self.settings.os)',
+                                  'prepend("user.myorg:myconf", self.settings.os)'])
+def test_error_bad_types(conf):
+    c = TestClient(light=True)
+    conanfile = textwrap.dedent(f"""
+        from conan import ConanFile
+        class Pkg(ConanFile):
+            name = "tool"
+            version = "0.1"
+            settings = "os"
+            options = {{"myopt": [True, False]}}
+            default_options = {{"myopt": True}}
+            def package_info(self):
+                self.conf_info.{conf}
+        """)
+    c.save({"conanfile.py": conanfile})
+    c.run("create .", assert_error=True)
+    assert "ERROR: tool/0.1: Error in package_info() method, line 10"
+    assert f'self.conf_info.{conf}' in c.out
+    assert "Invalid 'conf' type, please use Python types (int, str, ...)" in c.out
+