From aefca69be018a2956b8022de3daa6fee8bf8d4c8 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Sun, 26 Jan 2025 23:43:18 +0100 Subject: [PATCH 01/16] Parse nested JSON strings --- localstack_snapshot/snapshots/prototype.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/localstack_snapshot/snapshots/prototype.py b/localstack_snapshot/snapshots/prototype.py index 3ddc7b5..a86952c 100644 --- a/localstack_snapshot/snapshots/prototype.py +++ b/localstack_snapshot/snapshots/prototype.py @@ -271,10 +271,16 @@ def _transform_dict_to_parseable_values(self, original): if isinstance(v, Dict): self._transform_dict_to_parseable_values(v) - if isinstance(v, str) and v.startswith("{"): + if isinstance(v, str) and (v.startswith("{") or v.startswith("[")): try: json_value = json.loads(v) original[k] = json_value + if isinstance(json_value, list) and json_value: + for item in json_value: + if isinstance(item, dict): + self._transform_dict_to_parseable_values(item) + if isinstance(json_value, Dict): + self._transform_dict_to_parseable_values(json_value) except JSONDecodeError: pass # parsing error can be ignored From a42909a6908e1cefa63eef500a8e4ac46838e9a1 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Tue, 28 Jan 2025 12:58:43 +0100 Subject: [PATCH 02/16] Revert "Parse nested JSON strings" This reverts commit 32b9010ba2f8ba97fe962f56ad1f8870b492b55c. This change is somewhat nuclear option for existing snapshots, will be implemented as a separate transformer first. --- localstack_snapshot/snapshots/prototype.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/localstack_snapshot/snapshots/prototype.py b/localstack_snapshot/snapshots/prototype.py index a86952c..3ddc7b5 100644 --- a/localstack_snapshot/snapshots/prototype.py +++ b/localstack_snapshot/snapshots/prototype.py @@ -271,16 +271,10 @@ def _transform_dict_to_parseable_values(self, original): if isinstance(v, Dict): self._transform_dict_to_parseable_values(v) - if isinstance(v, str) and (v.startswith("{") or v.startswith("[")): + if isinstance(v, str) and v.startswith("{"): try: json_value = json.loads(v) original[k] = json_value - if isinstance(json_value, list) and json_value: - for item in json_value: - if isinstance(item, dict): - self._transform_dict_to_parseable_values(item) - if isinstance(json_value, Dict): - self._transform_dict_to_parseable_values(json_value) except JSONDecodeError: pass # parsing error can be ignored From bd8a4895c5d214607e943995e5eab9bf4a761d14 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Tue, 28 Jan 2025 13:01:56 +0100 Subject: [PATCH 03/16] Add JsonStringTransformer test --- localstack_snapshot/snapshots/transformer.py | 10 ++++++++++ tests/test_transformer.py | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/localstack_snapshot/snapshots/transformer.py b/localstack_snapshot/snapshots/transformer.py index a0a81c4..bd429f9 100644 --- a/localstack_snapshot/snapshots/transformer.py +++ b/localstack_snapshot/snapshots/transformer.py @@ -375,3 +375,13 @@ def replace_val(s): f"Registering text pattern '{self.text}' in snapshot with '{self.replacement}'" ) return input_data + + +class JsonStringTransformer: + key: str + + def __init__(self, key: str): + self.key = key + + def transform(self, input_data: dict, *, ctx: TransformContext) -> dict: + return input_data diff --git a/tests/test_transformer.py b/tests/test_transformer.py index 498266e..accf750 100644 --- a/tests/test_transformer.py +++ b/tests/test_transformer.py @@ -4,6 +4,7 @@ import pytest from localstack_snapshot.snapshots.transformer import ( + JsonStringTransformer, SortingTransformer, TimestampTransformer, TransformContext, @@ -311,6 +312,18 @@ def test_text(self, value): output = sr(output) assert json.loads(output) == expected + def test_json_string(self): + key = "key" + input = {key: '{"a":"b"}'} + expected = {key: {"a": "b"}} + + transformer = JsonStringTransformer(key) + + ctx = TransformContext() + output = transformer.transform(input, ctx=ctx) + + assert output == expected + class TestTimestampTransformer: def test_generic_timestamp_transformer(self): From 1e4c8836e494ce9adce51cb363448d352e676d8f Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Tue, 28 Jan 2025 13:04:04 +0100 Subject: [PATCH 04/16] Add JsonStringTransformer implementation --- localstack_snapshot/snapshots/transformer.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/localstack_snapshot/snapshots/transformer.py b/localstack_snapshot/snapshots/transformer.py index bd429f9..ab33ee0 100644 --- a/localstack_snapshot/snapshots/transformer.py +++ b/localstack_snapshot/snapshots/transformer.py @@ -1,8 +1,10 @@ import copy +import json import logging import os import re from datetime import datetime +from json import JSONDecodeError from re import Pattern from typing import Any, Callable, Optional, Protocol @@ -384,4 +386,14 @@ def __init__(self, key: str): self.key = key def transform(self, input_data: dict, *, ctx: TransformContext) -> dict: + for k, v in input_data.items(): + if k == self.key: + if isinstance(v, str): + try: + json_value = json.loads(v) + input_data[k] = json_value + except JSONDecodeError: + SNAPSHOT_LOGGER.warning( + f'The value mapped to "{k}" key is not a valid JSON string and won\'t be transformed' + ) return input_data From ac9803f1554b2df05fed13b9ef2f0dd7bfba9971 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Tue, 28 Jan 2025 16:11:12 +0100 Subject: [PATCH 05/16] Add tests for nested JSON strings --- tests/test_transformer.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/tests/test_transformer.py b/tests/test_transformer.py index accf750..d78c8f1 100644 --- a/tests/test_transformer.py +++ b/tests/test_transformer.py @@ -312,15 +312,33 @@ def test_text(self, value): output = sr(output) assert json.loads(output) == expected - def test_json_string(self): + @pytest.mark.parametrize( + "input_value,transformed_value", + [ + pytest.param('{"a": "b"}', {"a": "b"}, id="simple_json_object"), + pytest.param('{\n "a": "b"\n}', {"a": "b"}, id="formatted_json_object"), + pytest.param('{"a": 42}malformed', '{"a": 42}malformed', id="malformed_json"), + pytest.param('["a", "b"]', ["a", "b"], id="simple_json_list"), + pytest.param('{"a": "{\\"b\\":42}"}', {"a": {"b": 42}}, id="nested_json_object"), + pytest.param( + '{"a": "[{\\"b\\":\\"c\\"}]"}', {"a": [{"b": "c"}]}, id="nested_json_list" + ), + pytest.param( + '{"a": "{\\"b\\":42malformed}"}', + {"a": '{"b":42malformed}'}, + id="malformed_nested_json", + ), + ], + ) + def test_json_string(self, input_value, transformed_value): key = "key" - input = {key: '{"a":"b"}'} - expected = {key: {"a": "b"}} + input_data = {key: input_value} + expected = {key: transformed_value} transformer = JsonStringTransformer(key) ctx = TransformContext() - output = transformer.transform(input, ctx=ctx) + output = transformer.transform(input_data, ctx=ctx) assert output == expected From cbcdf8c1a24d2260f0083d631feade453fc0ef01 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Tue, 28 Jan 2025 16:13:50 +0100 Subject: [PATCH 06/16] Parse nested JSON strings --- localstack_snapshot/snapshots/transformer.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/localstack_snapshot/snapshots/transformer.py b/localstack_snapshot/snapshots/transformer.py index ab33ee0..955e357 100644 --- a/localstack_snapshot/snapshots/transformer.py +++ b/localstack_snapshot/snapshots/transformer.py @@ -391,9 +391,23 @@ def transform(self, input_data: dict, *, ctx: TransformContext) -> dict: if isinstance(v, str): try: json_value = json.loads(v) - input_data[k] = json_value + input_data[k] = self._transform_nested(json_value) except JSONDecodeError: SNAPSHOT_LOGGER.warning( f'The value mapped to "{k}" key is not a valid JSON string and won\'t be transformed' ) return input_data + + def _transform_nested(self, input_data: Any): + if isinstance(input_data, list): + input_data = [self._transform_nested(item) for item in input_data] + if isinstance(input_data, dict): + for k, v in input_data.items(): + input_data[k] = self._transform_nested(v) + if isinstance(input_data, str) and input_data.startswith(("{", "[")): + try: + json_value = json.loads(input_data) + input_data = self._transform_nested(json_value) + except JSONDecodeError: + pass # parsing nested JSON strings is a best effort rather than requirement, so no error message here + return input_data From 1ea3d2849ac511bd4c65acc288323937e7f721e6 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Tue, 28 Jan 2025 17:42:19 +0100 Subject: [PATCH 07/16] Add test for nested key --- tests/test_transformer.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_transformer.py b/tests/test_transformer.py index d78c8f1..9b75418 100644 --- a/tests/test_transformer.py +++ b/tests/test_transformer.py @@ -342,6 +342,18 @@ def test_json_string(self, input_value, transformed_value): assert output == expected + def test_json_string_in_a_nested_key(self): + key = "nested-key-in-an-object-hidden-inside-a-list" + input_data = {"top-level-key": [{key: '{"a": "b"}'}]} + expected = {"top-level-key": [{key: {"a": "b"}}]} + + transformer = JsonStringTransformer(key) + + ctx = TransformContext() + output = transformer.transform(input_data, ctx=ctx) + + assert output == expected + class TestTimestampTransformer: def test_generic_timestamp_transformer(self): From 4738d984acff0d7bc393625abc9ca4b2a1dcb84c Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Wed, 29 Jan 2025 03:12:54 +0100 Subject: [PATCH 08/16] Parse nested JSON string --- localstack_snapshot/snapshots/transformer.py | 40 +++++++++++++++----- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/localstack_snapshot/snapshots/transformer.py b/localstack_snapshot/snapshots/transformer.py index 955e357..1f45379 100644 --- a/localstack_snapshot/snapshots/transformer.py +++ b/localstack_snapshot/snapshots/transformer.py @@ -385,20 +385,40 @@ class JsonStringTransformer: def __init__(self, key: str): self.key = key - def transform(self, input_data: dict, *, ctx: TransformContext) -> dict: + def transform(self, input_data: dict, *, ctx: TransformContext = None) -> dict: + return self._transform_dict(input_data, ctx=ctx) + + def _transform(self, input_data: Any, ctx: TransformContext = None) -> Any: + if isinstance(input_data, dict): + return self._transform_dict(input_data, ctx=ctx) + elif isinstance(input_data, list): + return self._transform_list(input_data, ctx=ctx) + return input_data + + def _transform_dict(self, input_data: dict, ctx: TransformContext = None) -> dict: for k, v in input_data.items(): - if k == self.key: - if isinstance(v, str): - try: - json_value = json.loads(v) - input_data[k] = self._transform_nested(json_value) - except JSONDecodeError: - SNAPSHOT_LOGGER.warning( - f'The value mapped to "{k}" key is not a valid JSON string and won\'t be transformed' - ) + if k == self.key and isinstance(v, str): + try: + SNAPSHOT_LOGGER.debug(f"Replacing string value of {k} with parsed JSON") + json_value = json.loads(v) + input_data[k] = self._transform_nested(json_value) + except JSONDecodeError: + SNAPSHOT_LOGGER.warning( + f'The value mapped to "{k}" key is not a valid JSON string and won\'t be transformed' + ) + else: + input_data[k] = self._transform(v, ctx=ctx) return input_data + def _transform_list(self, input_data: list, ctx: TransformContext = None) -> list: + return [self._transform(item, ctx=ctx) for item in input_data] + def _transform_nested(self, input_data: Any): + """ + Attempts to parse any additional JSON strings inside parsed JSON + :param input_data: + :return: + """ if isinstance(input_data, list): input_data = [self._transform_nested(item) for item in input_data] if isinstance(input_data, dict): From fb2632ceabe0e9e36c8ecac3ad9054ed5fb99664 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Wed, 29 Jan 2025 11:56:11 +0100 Subject: [PATCH 09/16] Add docstrings --- localstack_snapshot/snapshots/transformer.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/localstack_snapshot/snapshots/transformer.py b/localstack_snapshot/snapshots/transformer.py index 1f45379..c9ad942 100644 --- a/localstack_snapshot/snapshots/transformer.py +++ b/localstack_snapshot/snapshots/transformer.py @@ -380,6 +380,11 @@ def replace_val(s): class JsonStringTransformer: + """ + Parses JSON string at key. + Additionally, attempts to parse any JSON strings inside the parsed JSON + """ + key: str def __init__(self, key: str): @@ -413,11 +418,12 @@ def _transform_dict(self, input_data: dict, ctx: TransformContext = None) -> dic def _transform_list(self, input_data: list, ctx: TransformContext = None) -> list: return [self._transform(item, ctx=ctx) for item in input_data] - def _transform_nested(self, input_data: Any): + def _transform_nested(self, input_data: Any) -> Any: """ - Attempts to parse any additional JSON strings inside parsed JSON - :param input_data: - :return: + Separate method from the main `_transform_dict` one because + it checks every string while the main one attempts to load at specified key only. + This one is implicit, best-effort attempt, + while the main one is explicit about at which key transform should happen """ if isinstance(input_data, list): input_data = [self._transform_nested(item) for item in input_data] From d700da8b7ab0a235e9d706b80d76f4a8ed319ece Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Wed, 29 Jan 2025 11:57:15 +0100 Subject: [PATCH 10/16] Improve logging --- localstack_snapshot/snapshots/transformer.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/localstack_snapshot/snapshots/transformer.py b/localstack_snapshot/snapshots/transformer.py index c9ad942..56977c1 100644 --- a/localstack_snapshot/snapshots/transformer.py +++ b/localstack_snapshot/snapshots/transformer.py @@ -408,8 +408,8 @@ def _transform_dict(self, input_data: dict, ctx: TransformContext = None) -> dic json_value = json.loads(v) input_data[k] = self._transform_nested(json_value) except JSONDecodeError: - SNAPSHOT_LOGGER.warning( - f'The value mapped to "{k}" key is not a valid JSON string and won\'t be transformed' + SNAPSHOT_LOGGER.exception( + f'Value mapped to "{k}" key is not a valid JSON string and won\'t be transformed. Value: {v}' ) else: input_data[k] = self._transform(v, ctx=ctx) @@ -435,5 +435,7 @@ def _transform_nested(self, input_data: Any) -> Any: json_value = json.loads(input_data) input_data = self._transform_nested(json_value) except JSONDecodeError: - pass # parsing nested JSON strings is a best effort rather than requirement, so no error message here + SNAPSHOT_LOGGER.debug( + f"The value is not a valid JSON string and won't be transformed. The value: {input_data}" + ) return input_data From 5429a914c27a05cf1240f5a3c7fd180ec0d7f074 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Wed, 29 Jan 2025 12:28:00 +0100 Subject: [PATCH 11/16] Handle possible whitespaces --- localstack_snapshot/snapshots/transformer.py | 2 +- tests/test_transformer.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/localstack_snapshot/snapshots/transformer.py b/localstack_snapshot/snapshots/transformer.py index 56977c1..0abe51a 100644 --- a/localstack_snapshot/snapshots/transformer.py +++ b/localstack_snapshot/snapshots/transformer.py @@ -430,7 +430,7 @@ def _transform_nested(self, input_data: Any) -> Any: if isinstance(input_data, dict): for k, v in input_data.items(): input_data[k] = self._transform_nested(v) - if isinstance(input_data, str) and input_data.startswith(("{", "[")): + if isinstance(input_data, str) and input_data.strip().startswith(("{", "[")): try: json_value = json.loads(input_data) input_data = self._transform_nested(json_value) diff --git a/tests/test_transformer.py b/tests/test_transformer.py index 9b75418..ae2d464 100644 --- a/tests/test_transformer.py +++ b/tests/test_transformer.py @@ -316,10 +316,16 @@ def test_text(self, value): "input_value,transformed_value", [ pytest.param('{"a": "b"}', {"a": "b"}, id="simple_json_object"), - pytest.param('{\n "a": "b"\n}', {"a": "b"}, id="formatted_json_object"), + pytest.param('{\n "a": "b"\n}', {"a": "b"}, id="formatted_json_object"), + pytest.param('\n {"a": "b"}', {"a": "b"}, id="json_with_whitespaces"), pytest.param('{"a": 42}malformed', '{"a": 42}malformed', id="malformed_json"), pytest.param('["a", "b"]', ["a", "b"], id="simple_json_list"), pytest.param('{"a": "{\\"b\\":42}"}', {"a": {"b": 42}}, id="nested_json_object"), + pytest.param( + '{"a": "\\n {\\n \\"b\\":42}"}', + {"a": {"b": 42}}, + id="nested_formatted_json_object_with_whitespaces", + ), pytest.param( '{"a": "[{\\"b\\":\\"c\\"}]"}', {"a": [{"b": "c"}]}, id="nested_json_list" ), From 32cce9fe3d5a5d4d600bb798238e38b0381015ac Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Wed, 29 Jan 2025 12:45:34 +0100 Subject: [PATCH 12/16] Add missing factory methods to TransformerUtility --- .../snapshots/transformer_utility.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/localstack_snapshot/snapshots/transformer_utility.py b/localstack_snapshot/snapshots/transformer_utility.py index 7a46651..65e412c 100644 --- a/localstack_snapshot/snapshots/transformer_utility.py +++ b/localstack_snapshot/snapshots/transformer_utility.py @@ -3,9 +3,11 @@ from localstack_snapshot.snapshots.transformer import ( JsonpathTransformer, + JsonStringTransformer, KeyValueBasedTransformer, KeyValueBasedTransformerFunctionReplacement, RegexTransformer, + SortingTransformer, TextTransformer, ) @@ -109,3 +111,27 @@ def text(text: str, replacement: str): :return: TextTransformer """ return TextTransformer(text, replacement) + + @staticmethod + def json_string(key: str) -> JsonStringTransformer: + """Creates a new JsonStringTransformer. If there is a valid JSON text string at specified key + it will be loaded as a regular object or array. + + :param key: key at which JSON string is expected + + :return: JsonStringTransformer + """ + return JsonStringTransformer(key) + + @staticmethod + def sorting(key: str, sorting_fn: Optional[Callable[[...], Any]]) -> SortingTransformer: + """Creates a new SortingTransformer. + + Sorts a list at `key` with the given `sorting_fn` (argument for `sorted(list, key=sorting_fn)`) + + :param key: key at which the list to sort is expected + :param sorting_fn: sorting function + + :return: SortingTransformer + """ + return SortingTransformer(key, sorting_fn) From 0f18a57f1232ef9ef7fb5d55df98c4d521effd33 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Wed, 29 Jan 2025 17:48:38 +0100 Subject: [PATCH 13/16] Add comment and TODO to common transformation flow --- localstack_snapshot/snapshots/prototype.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/localstack_snapshot/snapshots/prototype.py b/localstack_snapshot/snapshots/prototype.py index 3ddc7b5..533f285 100644 --- a/localstack_snapshot/snapshots/prototype.py +++ b/localstack_snapshot/snapshots/prototype.py @@ -272,6 +272,8 @@ def _transform_dict_to_parseable_values(self, original): self._transform_dict_to_parseable_values(v) if isinstance(v, str) and v.startswith("{"): + # Doesn't handle JSON arrays and nested JSON strings. See JsonStringTransformer. + # TODO for the major release consider having JSON parsing in one place only: either here or in JsonStringTransformer try: json_value = json.loads(v) original[k] = json_value From 257e3d91241f35a3521ba0e1b9597fdc6d917712 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Tue, 13 May 2025 23:59:31 +0200 Subject: [PATCH 14/16] Enhance transformer's docstring --- localstack_snapshot/snapshots/transformer.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/localstack_snapshot/snapshots/transformer.py b/localstack_snapshot/snapshots/transformer.py index 0abe51a..b537919 100644 --- a/localstack_snapshot/snapshots/transformer.py +++ b/localstack_snapshot/snapshots/transformer.py @@ -381,8 +381,21 @@ def replace_val(s): class JsonStringTransformer: """ - Parses JSON string at key. + Parses JSON string at the specified key. Additionally, attempts to parse any JSON strings inside the parsed JSON + + This transformer complements the default parsing of JSON strings in + localstack_snapshot.snapshots.prototype.SnapshotSession._transform_dict_to_parseable_values + + Shortcomings of the default parser that this transformer addresses: + - parsing of nested JSON strings '{"a": "{\\"b\\":42}"}' + - parsing of JSON arrays at the specified key, e.g. '["a", "b"]' + + Such parsing allows applying transformations further to the elements of the parsed JSON - timestamps, ARNs, etc. + + Such parsing is not done by default because it's not a common use case. + Whether to parse a JSON string or not should be decided by the user on a case by case basis. + Limited general parsing that we already have is preserved for backwards compatibility. """ key: str From 023423d7e122cdbebed307ea7b2b25ec5b916afd Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Wed, 14 May 2025 00:08:48 +0200 Subject: [PATCH 15/16] Add empty structures edge cases to tests --- tests/test_transformer.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_transformer.py b/tests/test_transformer.py index ae2d464..98a50d4 100644 --- a/tests/test_transformer.py +++ b/tests/test_transformer.py @@ -334,6 +334,9 @@ def test_text(self, value): {"a": '{"b":42malformed}'}, id="malformed_nested_json", ), + pytest.param("[]", [], id="empty_list"), + pytest.param("{}", {}, id="empty_object"), + pytest.param("", "", id="empty_string"), ], ) def test_json_string(self, input_value, transformed_value): From 6a56a9d8beb8aaa37b54ce3ad3faf551e3dd49f5 Mon Sep 17 00:00:00 2001 From: Misha Tiurin Date: Wed, 14 May 2025 00:31:29 +0200 Subject: [PATCH 16/16] Add sanity check before trying to load JSON at key Avoid verbose exception logging when not necessary. --- localstack_snapshot/snapshots/transformer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/localstack_snapshot/snapshots/transformer.py b/localstack_snapshot/snapshots/transformer.py index b537919..f0a6384 100644 --- a/localstack_snapshot/snapshots/transformer.py +++ b/localstack_snapshot/snapshots/transformer.py @@ -415,7 +415,7 @@ def _transform(self, input_data: Any, ctx: TransformContext = None) -> Any: def _transform_dict(self, input_data: dict, ctx: TransformContext = None) -> dict: for k, v in input_data.items(): - if k == self.key and isinstance(v, str): + if k == self.key and isinstance(v, str) and v.strip().startswith(("{", "[")): try: SNAPSHOT_LOGGER.debug(f"Replacing string value of {k} with parsed JSON") json_value = json.loads(v)