Repository: streamlit/streamlit

Issue:
component value of type dict is not properly marshalled with streamlit==0.84.0
### Summary

A component value returned from `_component_func` is of type `str` even if an **object** is set via `Streamlit.setComponentValue` in TypeScript code.
For example, when `{ someVal: 42 }` is set on TypeScript-side like `Streamlit.setComponentValue({ someVal: 42 })`,
the component value available on Python-side is a JSON string like `{"someVal":42}` (note, it's a string, not a dict).
This problem has emerged after upgrading Streamlit to 0.84.0.

### Steps to reproduce

We can reproduce this problem on https://github.com/streamlit/component-template/commit/bc6e70905beb8f9a178fc811603cc65b637bc38d with changes as below:
```diff
--- a/template/my_component/__init__.py
+++ b/template/my_component/__init__.py
@@ -70,11 +70,12 @@ def my_component(name, key=None):
     #
     # "default" is a special argument that specifies the initial return
     # value of the component before the user has interacted with it.
-    component_value = _component_func(name=name, key=key, default=0)
+    component_value = _component_func(name=name, key=key, default=None)
+    print(component_value, type(component_value))

     # We could modify the value returned from the component if we wanted.
     # There's no need to do this in our simple example - but it's an option.
-    return component_value
+    return component_value["numClicks"] if component_value else 0
```

```diff
--- a/template/my_component/frontend/src/MyComponent.tsx
+++ b/template/my_component/frontend/src/MyComponent.tsx
@@ -66,7 +66,7 @@ class MyComponent extends StreamlitComponentBase<State> {
     // Streamlit via `Streamlit.setComponentValue`.
     this.setState(
       prevState => ({ numClicks: prevState.numClicks + 1 }),
-      () => Streamlit.setComponentValue(this.state.numClicks)
+      () => Streamlit.setComponentValue({ numClicks: this.state.numClicks })
     )
   }
```

and the set-up steps described in https://github.com/streamlit/component-template#quickstart.
I also ran the following commands
* `pip install -U pip`
* `npm install typescript` twice to install the newer version of `tsc`.

**Expected behavior:**

The template works correctly


**Actual behavior:**

When you click the button, the error below appears.
<img width="852" alt="スクリーンショット 2021-07-03 1 26 57" src="https://user-images.githubusercontent.com/3135397/124303756-c6683a80-db9d-11eb-8e00-e606d8272a46.png">

The terminal output would be like below.
```shell
None <class 'NoneType'>
None <class 'NoneType'>
None <class 'NoneType'>
None <class 'NoneType'>
{"numClicks":1} <class 'str'>
2021-07-03 01:26:53.332 Traceback (most recent call last):
  File "/Users/whitphx/src/streamlit-component-sample/.venv/lib/python3.8/site-packages/streamlit/script_runner.py", line 349, in _run_script
    exec(code, module.__dict__)
  File "/Users/whitphx/src/streamlit-component-sample/my_component/__init__.py", line 91, in <module>
    num_clicks = my_component("World")
  File "/Users/whitphx/src/streamlit-component-sample/my_component/__init__.py", line 78, in my_component
    return component_value["numClicks"] if component_value else 0
TypeError: string indices must be integers
```

### Is this a regression?

That is, did this use to work the way you expected in the past?
yes

### Debug info

- Streamlit version: `Streamlit, version 0.84.0`
- Python version: `Python 3.8.7`
- Using Conda? PipEnv? PyEnv? Pex?: `pyenv` + `python -m venv`
- OS version: macOS Big Sur, 11.4
- Browser version: Vivaldi | 4.0.2312.33 (Stable channel) （arm64）


### Additional information

If needed, add any other context about the problem here. For example, did this bug come from https://discuss.streamlit.io or another site? Link the original source here!



Discussion:
Hey, thanks for reporting this. I have now investigated it and it seems we lost the json value special casing in the relevant code when we reimplemented it. I have a fix in mind that should restore the previous behavior, so it should work properly in the next release. 
Great find! I had users report the same on Drawable Canvas 😀 will follow this too

**EDIT**: forum links to ping when this is closed:

* https://discuss.streamlit.io/t/streamlit-ag-grid-doesnt-work-anymore-with-streamlit-v0-84-0/14707 
* https://discuss.streamlit.io/t/ag-grid-component-with-input-support/8108/159
* https://discuss.streamlit.io/t/version-0-84-0/14542/8
* https://discuss.streamlit.io/t/drawable-canvas/3671/29
https://github.com/domoritz/streamlit-vega-lite-demo/issues/2 looks like it's caused by this as well. 

Resolving patch:
diff --git a/lib/streamlit/components/v1/components.py b/lib/streamlit/components/v1/components.py
--- a/lib/streamlit/components/v1/components.py
+++ b/lib/streamlit/components/v1/components.py
@@ -185,6 +185,7 @@ def marshall_element_args():
                 marshall_element_args()
 
             def deserialize_component(ui_value, widget_id=""):
+                # ui_value is an object from json, an ArrowTable proto, or a bytearray
                 return ui_value
 
             widget_value, _ = register_widget(
diff --git a/lib/streamlit/state/session_state.py b/lib/streamlit/state/session_state.py
--- a/lib/streamlit/state/session_state.py
+++ b/lib/streamlit/state/session_state.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import json
 from typing import (
     Any,
     ItemsView,
@@ -96,16 +97,25 @@ def __getitem__(self, k: str) -> Any:
                     # No deserializer, which should only happen if state is gotten from a reconnecting browser
                     # and the script is trying to access it. Pretend it doesn't exist.
                     raise KeyError(k)
-                value = item.value.__getattribute__(item.value.WhichOneof("value"))
+                value_type = item.value.WhichOneof("value")
+                value = item.value.__getattribute__(value_type)
 
                 # Array types are messages with data in a `data` field
-                if metadata.value_type in [
+                if value_type in [
                     "double_array_value",
                     "int_array_value",
                     "string_array_value",
                 ]:
                     value = value.data
+
+                if value_type == "json_value":
+                    value = json.loads(value)
+
                 deserialized = metadata.deserializer(value, metadata.id)
+
+                # Update metadata to reflect information from WidgetState proto
+                self.set_widget_metadata(attr.evolve(metadata, value_type=value_type))
+
                 self.states[k] = Value(deserialized)
                 return deserialized
         else:
@@ -174,6 +184,8 @@ def get_serialized(
                     ):
                         arr = getattr(widget, field)
                         arr.data.extend(serialized)
+                    elif field == "json_value":
+                        setattr(widget, field, json.dumps(serialized))
                     else:
                         setattr(widget, field, serialized)
                     return widget


Test patch:
diff --git a/lib/tests/streamlit/state/session_state_test.py b/lib/tests/streamlit/state/session_state_test.py
--- a/lib/tests/streamlit/state/session_state_test.py
+++ b/lib/tests/streamlit/state/session_state_test.py
@@ -70,6 +70,23 @@ def setUp(self):
             )
         )
 
+    def test_get_from_json_value(self):
+        widget_state = WidgetStateProto()
+        widget_state.id = "widget_id_3"
+        widget_state.json_value = '{"foo":5}'
+
+        self.wstates.set_from_proto(widget_state)
+        self.wstates.set_widget_metadata(
+            WidgetMetadata(
+                id="widget_id_3",
+                deserializer=lambda x, s: x,
+                serializer=identity,
+                value_type="json_value",
+            )
+        )
+
+        assert self.wstates["widget_id_3"] == {"foo": 5}
+
     def test_getitem_nonexistent(self):
         with pytest.raises(KeyError):
             self.wstates["nonexistent_widget_id"]
@@ -146,6 +163,21 @@ def test_get_serialized_array_value(self):
         assert serialized.id == "widget_id_1"
         assert list(serialized.int_array_value.data) == [1, 2, 3, 4]
 
+    def test_get_serialized_json_value(self):
+        self.wstates.set_from_value("widget_id_3", {"foo": 5})
+        self.wstates.set_widget_metadata(
+            WidgetMetadata(
+                id="widget_id_3",
+                deserializer=lambda x, s: x,
+                serializer=identity,
+                value_type="json_value",
+            )
+        )
+
+        serialized = self.wstates.get_serialized("widget_id_3")
+        assert serialized.id == "widget_id_3"
+        assert serialized.json_value == '{"foo": 5}'
+
     def test_as_widget_states(self):
         widget_states = self.wstates.as_widget_states()
         assert len(widget_states) == 2
@@ -459,7 +491,7 @@ def test_delattr(self):
         del self.session_state.foo
         assert "foo" not in self.session_state
 
-    def test_delitem_errors(self):
+    def test_delitem_attr_errors(self):
         for key in ["_new_session_state", "_new_widget_state", "_old_state"]:
             with pytest.raises(AttributeError):
                 delattr(st.session_state, key)