From 4604c0c0ffb720339355843cf1fb1e620c3d6abb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Jan 2026 08:43:46 +0000 Subject: [PATCH 2/8] Add preservingProtoFieldNames() to fix snake_case to camelCase conversion Co-authored-by: ViacheslavKlimov <56742475+ViacheslavKlimov@users.noreply.github.com> --- .../server/common/data/DynamicProtoUtils.java | 2 +- .../common/data/DynamicProtoUtilsTest.java | 46 +++++++++++++++++++ .../queue/common/TbProtoJsQueueMsg.java | 2 +- .../script/RemoteJsRequestEncoder.java | 2 +- 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DynamicProtoUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/DynamicProtoUtils.java index 2a482a0e05..c18bef2837 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DynamicProtoUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DynamicProtoUtils.java @@ -99,7 +99,7 @@ public class DynamicProtoUtils { public static String dynamicMsgToJson(Descriptors.Descriptor descriptor, byte[] payload) throws InvalidProtocolBufferException { DynamicMessage dynamicMessage = DynamicMessage.parseFrom(descriptor, payload); - return JsonFormat.printer().includingDefaultValueFields().print(dynamicMessage); + return JsonFormat.printer().preservingProtoFieldNames().includingDefaultValueFields().print(dynamicMessage); } public static DynamicMessage jsonToDynamicMessage(DynamicMessage.Builder builder, String payload) throws InvalidProtocolBufferException { diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java index 4b710f9a60..94feaab091 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java @@ -166,4 +166,50 @@ public class DynamicProtoUtilsTest { DynamicProtoUtils.dynamicMsgToJson(sampleMsgDescriptor, sampleMsgWithOneOfSubMessage.toByteArray())); } + @Test + public void testProtoSchemaPreservesSnakeCaseFieldNames() throws Exception { + String schema = "syntax = \"proto3\";\n" + + "\n" + + "package firmware;\n" + + "\n" + + "message FirmwareStatus {\n" + + " string current_fw_title = 1;\n" + + " string current_fw_version = 2;\n" + + " string fw_state = 3;\n" + + " string target_fw_title = 4;\n" + + " string target_fw_version = 5;\n" + + "}"; + ProtoFileElement protoFileElement = DynamicProtoUtils.getProtoFileElement(schema); + DynamicSchema dynamicSchema = DynamicProtoUtils.getDynamicSchema(protoFileElement, "test schema with snake_case fields"); + assertNotNull(dynamicSchema); + + DynamicMessage.Builder firmwareStatusBuilder = dynamicSchema.newMessageBuilder("firmware.FirmwareStatus"); + Descriptors.Descriptor firmwareStatusDescriptor = firmwareStatusBuilder.getDescriptorForType(); + assertNotNull(firmwareStatusDescriptor); + + DynamicMessage firmwareStatus = firmwareStatusBuilder + .setField(firmwareStatusDescriptor.findFieldByName("current_fw_title"), "firmware_v1") + .setField(firmwareStatusDescriptor.findFieldByName("current_fw_version"), "1.0.0") + .setField(firmwareStatusDescriptor.findFieldByName("fw_state"), "DOWNLOADING") + .setField(firmwareStatusDescriptor.findFieldByName("target_fw_title"), "firmware_v2") + .setField(firmwareStatusDescriptor.findFieldByName("target_fw_version"), "2.0.0") + .build(); + + String json = DynamicProtoUtils.dynamicMsgToJson(firmwareStatusDescriptor, firmwareStatus.toByteArray()); + + // Verify that snake_case field names are preserved (not converted to camelCase) + assertTrue("JSON should contain snake_case field 'current_fw_title'", json.contains("\"current_fw_title\"")); + assertTrue("JSON should contain snake_case field 'current_fw_version'", json.contains("\"current_fw_version\"")); + assertTrue("JSON should contain snake_case field 'fw_state'", json.contains("\"fw_state\"")); + assertTrue("JSON should contain snake_case field 'target_fw_title'", json.contains("\"target_fw_title\"")); + assertTrue("JSON should contain snake_case field 'target_fw_version'", json.contains("\"target_fw_version\"")); + + // Verify that camelCase versions are NOT present + assertTrue("JSON should NOT contain camelCase field 'currentFwTitle'", !json.contains("\"currentFwTitle\"")); + assertTrue("JSON should NOT contain camelCase field 'currentFwVersion'", !json.contains("\"currentFwVersion\"")); + assertTrue("JSON should NOT contain camelCase field 'fwState'", !json.contains("\"fwState\"")); + assertTrue("JSON should NOT contain camelCase field 'targetFwTitle'", !json.contains("\"targetFwTitle\"")); + assertTrue("JSON should NOT contain camelCase field 'targetFwVersion'", !json.contains("\"targetFwVersion\"")); + } + } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/TbProtoJsQueueMsg.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/TbProtoJsQueueMsg.java index 10bf1d2a44..0b8cf0c789 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/TbProtoJsQueueMsg.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/TbProtoJsQueueMsg.java @@ -35,7 +35,7 @@ public class TbProtoJsQueueMsg @Override public byte[] getData() { try { - return JsonFormat.printer().print(value).getBytes(StandardCharsets.UTF_8); + return JsonFormat.printer().preservingProtoFieldNames().print(value).getBytes(StandardCharsets.UTF_8); } catch (InvalidProtocolBufferException e) { throw new RuntimeException(e); } diff --git a/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java b/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java index adef547068..c72ea3b717 100644 --- a/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java +++ b/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java @@ -30,7 +30,7 @@ public class RemoteJsRequestEncoder implements TbKafkaEncoder value) { try { - return JsonFormat.printer().print(value.getValue()).getBytes(StandardCharsets.UTF_8); + return JsonFormat.printer().preservingProtoFieldNames().print(value.getValue()).getBytes(StandardCharsets.UTF_8); } catch (InvalidProtocolBufferException e) { throw new RuntimeException(e); } From 63f7ef2d80593d59cb389775fcb5df8e61e5ab64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Jan 2026 08:46:06 +0000 Subject: [PATCH 3/8] Fix LwM2M JsonFormat.printer() to preserve field names for consistency Co-authored-by: ViacheslavKlimov <56742475+ViacheslavKlimov@users.noreply.github.com> --- .../transport/lwm2m/server/store/util/LwM2MClientSerDes.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java index 6b5bc75306..d9b4d44988 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java @@ -71,7 +71,7 @@ public class LwM2MClientSerDes { JsonObject sharedAttributes = new JsonObject(); for (Map.Entry entry : client.getSharedAttributes().entrySet()) { - sharedAttributes.addProperty(entry.getKey(), JsonFormat.printer().print(entry.getValue())); + sharedAttributes.addProperty(entry.getKey(), JsonFormat.printer().preservingProtoFieldNames().print(entry.getValue())); } o.add("sharedAttributes", sharedAttributes); @@ -84,7 +84,7 @@ public class LwM2MClientSerDes { o.addProperty("state", client.getState().toString()); if (client.getSession() != null) { - o.addProperty("session", JsonFormat.printer().print(client.getSession())); + o.addProperty("session", JsonFormat.printer().preservingProtoFieldNames().print(client.getSession())); } if (client.getTenantId() != null) { o.addProperty("tenantId", client.getTenantId().toString()); From 46210183e9cd3958cc41fcdb2214d047987b366c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Jan 2026 08:47:21 +0000 Subject: [PATCH 4/8] Improve test assertions to use assertFalse instead of negated assertTrue Co-authored-by: ViacheslavKlimov <56742475+ViacheslavKlimov@users.noreply.github.com> --- .../server/common/data/DynamicProtoUtilsTest.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java index 94feaab091..931399288a 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -205,11 +206,11 @@ public class DynamicProtoUtilsTest { assertTrue("JSON should contain snake_case field 'target_fw_version'", json.contains("\"target_fw_version\"")); // Verify that camelCase versions are NOT present - assertTrue("JSON should NOT contain camelCase field 'currentFwTitle'", !json.contains("\"currentFwTitle\"")); - assertTrue("JSON should NOT contain camelCase field 'currentFwVersion'", !json.contains("\"currentFwVersion\"")); - assertTrue("JSON should NOT contain camelCase field 'fwState'", !json.contains("\"fwState\"")); - assertTrue("JSON should NOT contain camelCase field 'targetFwTitle'", !json.contains("\"targetFwTitle\"")); - assertTrue("JSON should NOT contain camelCase field 'targetFwVersion'", !json.contains("\"targetFwVersion\"")); + assertFalse("JSON should NOT contain camelCase field 'currentFwTitle'", json.contains("\"currentFwTitle\"")); + assertFalse("JSON should NOT contain camelCase field 'currentFwVersion'", json.contains("\"currentFwVersion\"")); + assertFalse("JSON should NOT contain camelCase field 'fwState'", json.contains("\"fwState\"")); + assertFalse("JSON should NOT contain camelCase field 'targetFwTitle'", json.contains("\"targetFwTitle\"")); + assertFalse("JSON should NOT contain camelCase field 'targetFwVersion'", json.contains("\"targetFwVersion\"")); } } From 4b38a7784e016ad469b7910df87e600b1c72ad69 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Jan 2026 08:57:35 +0000 Subject: [PATCH 5/8] Make proto field name preservation configurable via env variable with backward compatibility Co-authored-by: ViacheslavKlimov <56742475+ViacheslavKlimov@users.noreply.github.com> --- .../src/main/resources/thingsboard.yml | 5 +++ .../server/common/data/DynamicProtoUtils.java | 7 +++- .../common/data/DynamicProtoUtilsTest.java | 33 +++++++++++-------- .../queue/common/TbProtoJsQueueMsg.java | 7 +++- .../script/RemoteJsRequestEncoder.java | 8 ++++- .../server/store/util/LwM2MClientSerDes.java | 9 +++-- 6 files changed, 51 insertions(+), 18 deletions(-) diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 63c461dd7c..276a9aee2f 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -1049,6 +1049,11 @@ transport: type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}" # Maximum allowed string value length when processing Telemetry/Attributes JSON (0 value disables string value length check) max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}" + # Preserve proto field names (e.g., 'current_fw_title') instead of converting to camelCase (e.g., 'currentFwTitle') when processing Protobuf messages. + # When set to 'false' (default), field names are converted to camelCase for backward compatibility. + # When set to 'true', field names are preserved as defined in the .proto schema. + # This affects MQTT, CoAP, and LwM2M transports using Protobuf payload. + preserve_proto_field_names: "${TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES:false}" client_side_rpc: # Processing timeout interval of the RPC command on the CLIENT SIDE. Time in milliseconds timeout: "${CLIENT_SIDE_RPC_TIMEOUT:60000}" diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DynamicProtoUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/DynamicProtoUtils.java index c18bef2837..06f79bda3a 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DynamicProtoUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DynamicProtoUtils.java @@ -45,6 +45,10 @@ public class DynamicProtoUtils { public static final Location LOCATION = new Location("", "", -1, -1); public static final String PROTO_3_SYNTAX = "proto3"; + + private static final JsonFormat.Printer JSON_PRINTER = JsonFormat.printer().includingDefaultValueFields(); + private static final JsonFormat.Printer JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES = JsonFormat.printer().preservingProtoFieldNames().includingDefaultValueFields(); + private static final boolean PRESERVE_PROTO_FIELD_NAMES = Boolean.parseBoolean(System.getProperty("transport.json.preserve_proto_field_names", System.getenv("TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES"))); public static Descriptors.Descriptor getDescriptor(String protoSchema, String schemaName) { try { @@ -99,7 +103,8 @@ public class DynamicProtoUtils { public static String dynamicMsgToJson(Descriptors.Descriptor descriptor, byte[] payload) throws InvalidProtocolBufferException { DynamicMessage dynamicMessage = DynamicMessage.parseFrom(descriptor, payload); - return JsonFormat.printer().preservingProtoFieldNames().includingDefaultValueFields().print(dynamicMessage); + JsonFormat.Printer printer = PRESERVE_PROTO_FIELD_NAMES ? JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES : JSON_PRINTER; + return printer.print(dynamicMessage); } public static DynamicMessage jsonToDynamicMessage(DynamicMessage.Builder builder, String payload) throws InvalidProtocolBufferException { diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java index 931399288a..ec164b7eb3 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java @@ -168,7 +168,9 @@ public class DynamicProtoUtilsTest { } @Test - public void testProtoSchemaPreservesSnakeCaseFieldNames() throws Exception { + public void testProtoSchemaDefaultBehaviorConvertsToCamelCase() throws Exception { + // By default (when TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES is not set), + // field names should be converted to camelCase for backward compatibility String schema = "syntax = \"proto3\";\n" + "\n" + "package firmware;\n" + @@ -198,19 +200,24 @@ public class DynamicProtoUtilsTest { String json = DynamicProtoUtils.dynamicMsgToJson(firmwareStatusDescriptor, firmwareStatus.toByteArray()); - // Verify that snake_case field names are preserved (not converted to camelCase) - assertTrue("JSON should contain snake_case field 'current_fw_title'", json.contains("\"current_fw_title\"")); - assertTrue("JSON should contain snake_case field 'current_fw_version'", json.contains("\"current_fw_version\"")); - assertTrue("JSON should contain snake_case field 'fw_state'", json.contains("\"fw_state\"")); - assertTrue("JSON should contain snake_case field 'target_fw_title'", json.contains("\"target_fw_title\"")); - assertTrue("JSON should contain snake_case field 'target_fw_version'", json.contains("\"target_fw_version\"")); + // By default, field names should be converted to camelCase (backward compatible behavior) + boolean preserveFieldNames = Boolean.parseBoolean(System.getProperty("transport.json.preserve_proto_field_names", System.getenv("TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES"))); - // Verify that camelCase versions are NOT present - assertFalse("JSON should NOT contain camelCase field 'currentFwTitle'", json.contains("\"currentFwTitle\"")); - assertFalse("JSON should NOT contain camelCase field 'currentFwVersion'", json.contains("\"currentFwVersion\"")); - assertFalse("JSON should NOT contain camelCase field 'fwState'", json.contains("\"fwState\"")); - assertFalse("JSON should NOT contain camelCase field 'targetFwTitle'", json.contains("\"targetFwTitle\"")); - assertFalse("JSON should NOT contain camelCase field 'targetFwVersion'", json.contains("\"targetFwVersion\"")); + if (preserveFieldNames) { + // When flag is enabled, verify snake_case is preserved + assertTrue("JSON should contain snake_case field 'current_fw_title'", json.contains("\"current_fw_title\"")); + assertTrue("JSON should contain snake_case field 'current_fw_version'", json.contains("\"current_fw_version\"")); + assertTrue("JSON should contain snake_case field 'fw_state'", json.contains("\"fw_state\"")); + assertTrue("JSON should contain snake_case field 'target_fw_title'", json.contains("\"target_fw_title\"")); + assertTrue("JSON should contain snake_case field 'target_fw_version'", json.contains("\"target_fw_version\"")); + } else { + // Default behavior: field names converted to camelCase + assertTrue("JSON should contain camelCase field 'currentFwTitle'", json.contains("\"currentFwTitle\"")); + assertTrue("JSON should contain camelCase field 'currentFwVersion'", json.contains("\"currentFwVersion\"")); + assertTrue("JSON should contain camelCase field 'fwState'", json.contains("\"fwState\"")); + assertTrue("JSON should contain camelCase field 'targetFwTitle'", json.contains("\"targetFwTitle\"")); + assertTrue("JSON should contain camelCase field 'targetFwVersion'", json.contains("\"targetFwVersion\"")); + } } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/TbProtoJsQueueMsg.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/TbProtoJsQueueMsg.java index 0b8cf0c789..eafa770659 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/TbProtoJsQueueMsg.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/TbProtoJsQueueMsg.java @@ -24,6 +24,10 @@ import java.util.UUID; public class TbProtoJsQueueMsg extends TbProtoQueueMsg { + private static final JsonFormat.Printer JSON_PRINTER = JsonFormat.printer(); + private static final JsonFormat.Printer JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES = JsonFormat.printer().preservingProtoFieldNames(); + private static final boolean PRESERVE_PROTO_FIELD_NAMES = Boolean.parseBoolean(System.getProperty("transport.json.preserve_proto_field_names", System.getenv("TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES"))); + public TbProtoJsQueueMsg(UUID key, T value) { super(key, value); } @@ -35,7 +39,8 @@ public class TbProtoJsQueueMsg @Override public byte[] getData() { try { - return JsonFormat.printer().preservingProtoFieldNames().print(value).getBytes(StandardCharsets.UTF_8); + JsonFormat.Printer printer = PRESERVE_PROTO_FIELD_NAMES ? JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES : JSON_PRINTER; + return printer.print(value).getBytes(StandardCharsets.UTF_8); } catch (InvalidProtocolBufferException e) { throw new RuntimeException(e); } diff --git a/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java b/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java index c72ea3b717..ef68d4fbc9 100644 --- a/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java +++ b/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java @@ -27,10 +27,16 @@ import java.nio.charset.StandardCharsets; * Created by ashvayka on 25.09.18. */ public class RemoteJsRequestEncoder implements TbKafkaEncoder> { + + private static final JsonFormat.Printer JSON_PRINTER = JsonFormat.printer(); + private static final JsonFormat.Printer JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES = JsonFormat.printer().preservingProtoFieldNames(); + private static final boolean PRESERVE_PROTO_FIELD_NAMES = Boolean.parseBoolean(System.getProperty("transport.json.preserve_proto_field_names", System.getenv("TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES"))); + @Override public byte[] encode(TbProtoQueueMsg value) { try { - return JsonFormat.printer().preservingProtoFieldNames().print(value.getValue()).getBytes(StandardCharsets.UTF_8); + JsonFormat.Printer printer = PRESERVE_PROTO_FIELD_NAMES ? JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES : JSON_PRINTER; + return printer.print(value.getValue()).getBytes(StandardCharsets.UTF_8); } catch (InvalidProtocolBufferException e) { throw new RuntimeException(e); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java index d9b4d44988..026844b160 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java @@ -53,6 +53,9 @@ import static org.thingsboard.common.util.JacksonUtil.toJsonNode; public class LwM2MClientSerDes { public static final String VALUE = "value"; private static final RegistrationSerDes registrationSerDes = new RegistrationSerDes(); + private static final JsonFormat.Printer JSON_PRINTER = JsonFormat.printer(); + private static final JsonFormat.Printer JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES = JsonFormat.printer().preservingProtoFieldNames(); + private static final boolean PRESERVE_PROTO_FIELD_NAMES = Boolean.parseBoolean(System.getProperty("transport.json.preserve_proto_field_names", System.getenv("TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES"))); @SneakyThrows public static byte[] serialize(LwM2mClient client) { @@ -71,7 +74,8 @@ public class LwM2MClientSerDes { JsonObject sharedAttributes = new JsonObject(); for (Map.Entry entry : client.getSharedAttributes().entrySet()) { - sharedAttributes.addProperty(entry.getKey(), JsonFormat.printer().preservingProtoFieldNames().print(entry.getValue())); + JsonFormat.Printer printer = PRESERVE_PROTO_FIELD_NAMES ? JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES : JSON_PRINTER; + sharedAttributes.addProperty(entry.getKey(), printer.print(entry.getValue())); } o.add("sharedAttributes", sharedAttributes); @@ -84,7 +88,8 @@ public class LwM2MClientSerDes { o.addProperty("state", client.getState().toString()); if (client.getSession() != null) { - o.addProperty("session", JsonFormat.printer().preservingProtoFieldNames().print(client.getSession())); + JsonFormat.Printer printer = PRESERVE_PROTO_FIELD_NAMES ? JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES : JSON_PRINTER; + o.addProperty("session", printer.print(client.getSession())); } if (client.getTenantId() != null) { o.addProperty("tenantId", client.getTenantId().toString()); From cc57bd3fd2597a070d51c2bf8458ba512b742b75 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Jan 2026 09:11:19 +0000 Subject: [PATCH 6/8] Address review feedback: limit preservingProtoFieldNames to dynamic proto only and add config to all transport ymls Co-authored-by: ViacheslavKlimov <56742475+ViacheslavKlimov@users.noreply.github.com> --- .../common/data/DynamicProtoUtilsTest.java | 77 ++++++++++++++++--- .../queue/common/TbProtoJsQueueMsg.java | 7 +- .../script/RemoteJsRequestEncoder.java | 8 +- .../server/store/util/LwM2MClientSerDes.java | 9 +-- .../src/main/resources/tb-coap-transport.yml | 5 ++ .../src/main/resources/tb-http-transport.yml | 5 ++ .../src/main/resources/tb-lwm2m-transport.yml | 5 ++ .../src/main/resources/tb-mqtt-transport.yml | 5 ++ .../src/main/resources/tb-snmp-transport.yml | 5 ++ 9 files changed, 97 insertions(+), 29 deletions(-) diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java index ec164b7eb3..20cf454fc4 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java @@ -169,8 +169,8 @@ public class DynamicProtoUtilsTest { @Test public void testProtoSchemaDefaultBehaviorConvertsToCamelCase() throws Exception { - // By default (when TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES is not set), - // field names should be converted to camelCase for backward compatibility + // Test default behavior when TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES is not set + // Field names should be converted to camelCase for backward compatibility String schema = "syntax = \"proto3\";\n" + "\n" + "package firmware;\n" + @@ -200,7 +200,63 @@ public class DynamicProtoUtilsTest { String json = DynamicProtoUtils.dynamicMsgToJson(firmwareStatusDescriptor, firmwareStatus.toByteArray()); - // By default, field names should be converted to camelCase (backward compatible behavior) + // Check the actual behavior based on the current flag setting + boolean preserveFieldNames = Boolean.parseBoolean(System.getProperty("transport.json.preserve_proto_field_names", System.getenv("TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES"))); + + if (!preserveFieldNames) { + // Default behavior: field names converted to camelCase + assertTrue("JSON should contain camelCase field 'currentFwTitle'", json.contains("\"currentFwTitle\"")); + assertTrue("JSON should contain camelCase field 'currentFwVersion'", json.contains("\"currentFwVersion\"")); + assertTrue("JSON should contain camelCase field 'fwState'", json.contains("\"fwState\"")); + assertTrue("JSON should contain camelCase field 'targetFwTitle'", json.contains("\"targetFwTitle\"")); + assertTrue("JSON should contain camelCase field 'targetFwVersion'", json.contains("\"targetFwVersion\"")); + + // Verify snake_case versions are NOT present + assertFalse("JSON should NOT contain snake_case field 'current_fw_title'", json.contains("\"current_fw_title\"")); + assertFalse("JSON should NOT contain snake_case field 'fw_state'", json.contains("\"fw_state\"")); + } else { + // This test is designed to verify default behavior, skip if flag is set + // The next test will verify the preserve behavior + assertTrue("This test expects default behavior (camelCase conversion). " + + "Set TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES=false or run without the flag.", + !preserveFieldNames); + } + } + + @Test + public void testProtoSchemaPreservesSnakeCaseFieldNamesWhenEnabled() throws Exception { + // Test behavior when TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES is set to true + // Field names should be preserved as defined in .proto schema + String schema = "syntax = \"proto3\";\n" + + "\n" + + "package firmware;\n" + + "\n" + + "message FirmwareStatus {\n" + + " string current_fw_title = 1;\n" + + " string current_fw_version = 2;\n" + + " string fw_state = 3;\n" + + " string target_fw_title = 4;\n" + + " string target_fw_version = 5;\n" + + "}"; + ProtoFileElement protoFileElement = DynamicProtoUtils.getProtoFileElement(schema); + DynamicSchema dynamicSchema = DynamicProtoUtils.getDynamicSchema(protoFileElement, "test schema with snake_case fields"); + assertNotNull(dynamicSchema); + + DynamicMessage.Builder firmwareStatusBuilder = dynamicSchema.newMessageBuilder("firmware.FirmwareStatus"); + Descriptors.Descriptor firmwareStatusDescriptor = firmwareStatusBuilder.getDescriptorForType(); + assertNotNull(firmwareStatusDescriptor); + + DynamicMessage firmwareStatus = firmwareStatusBuilder + .setField(firmwareStatusDescriptor.findFieldByName("current_fw_title"), "firmware_v1") + .setField(firmwareStatusDescriptor.findFieldByName("current_fw_version"), "1.0.0") + .setField(firmwareStatusDescriptor.findFieldByName("fw_state"), "DOWNLOADING") + .setField(firmwareStatusDescriptor.findFieldByName("target_fw_title"), "firmware_v2") + .setField(firmwareStatusDescriptor.findFieldByName("target_fw_version"), "2.0.0") + .build(); + + String json = DynamicProtoUtils.dynamicMsgToJson(firmwareStatusDescriptor, firmwareStatus.toByteArray()); + + // Check the actual behavior based on the current flag setting boolean preserveFieldNames = Boolean.parseBoolean(System.getProperty("transport.json.preserve_proto_field_names", System.getenv("TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES"))); if (preserveFieldNames) { @@ -210,13 +266,16 @@ public class DynamicProtoUtilsTest { assertTrue("JSON should contain snake_case field 'fw_state'", json.contains("\"fw_state\"")); assertTrue("JSON should contain snake_case field 'target_fw_title'", json.contains("\"target_fw_title\"")); assertTrue("JSON should contain snake_case field 'target_fw_version'", json.contains("\"target_fw_version\"")); + + // Verify camelCase versions are NOT present + assertFalse("JSON should NOT contain camelCase field 'currentFwTitle'", json.contains("\"currentFwTitle\"")); + assertFalse("JSON should NOT contain camelCase field 'fwState'", json.contains("\"fwState\"")); } else { - // Default behavior: field names converted to camelCase - assertTrue("JSON should contain camelCase field 'currentFwTitle'", json.contains("\"currentFwTitle\"")); - assertTrue("JSON should contain camelCase field 'currentFwVersion'", json.contains("\"currentFwVersion\"")); - assertTrue("JSON should contain camelCase field 'fwState'", json.contains("\"fwState\"")); - assertTrue("JSON should contain camelCase field 'targetFwTitle'", json.contains("\"targetFwTitle\"")); - assertTrue("JSON should contain camelCase field 'targetFwVersion'", json.contains("\"targetFwVersion\"")); + // This test is designed to verify preserve behavior, skip if flag is not set + // Run this test with: TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES=true + assertTrue("This test expects preserve behavior (snake_case preservation). " + + "Set TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES=true to run this test.", + preserveFieldNames); } } diff --git a/common/queue/src/main/java/org/thingsboard/server/queue/common/TbProtoJsQueueMsg.java b/common/queue/src/main/java/org/thingsboard/server/queue/common/TbProtoJsQueueMsg.java index eafa770659..10bf1d2a44 100644 --- a/common/queue/src/main/java/org/thingsboard/server/queue/common/TbProtoJsQueueMsg.java +++ b/common/queue/src/main/java/org/thingsboard/server/queue/common/TbProtoJsQueueMsg.java @@ -24,10 +24,6 @@ import java.util.UUID; public class TbProtoJsQueueMsg extends TbProtoQueueMsg { - private static final JsonFormat.Printer JSON_PRINTER = JsonFormat.printer(); - private static final JsonFormat.Printer JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES = JsonFormat.printer().preservingProtoFieldNames(); - private static final boolean PRESERVE_PROTO_FIELD_NAMES = Boolean.parseBoolean(System.getProperty("transport.json.preserve_proto_field_names", System.getenv("TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES"))); - public TbProtoJsQueueMsg(UUID key, T value) { super(key, value); } @@ -39,8 +35,7 @@ public class TbProtoJsQueueMsg @Override public byte[] getData() { try { - JsonFormat.Printer printer = PRESERVE_PROTO_FIELD_NAMES ? JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES : JSON_PRINTER; - return printer.print(value).getBytes(StandardCharsets.UTF_8); + return JsonFormat.printer().print(value).getBytes(StandardCharsets.UTF_8); } catch (InvalidProtocolBufferException e) { throw new RuntimeException(e); } diff --git a/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java b/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java index ef68d4fbc9..adef547068 100644 --- a/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java +++ b/common/script/remote-js-client/src/main/java/org/thingsboard/server/service/script/RemoteJsRequestEncoder.java @@ -27,16 +27,10 @@ import java.nio.charset.StandardCharsets; * Created by ashvayka on 25.09.18. */ public class RemoteJsRequestEncoder implements TbKafkaEncoder> { - - private static final JsonFormat.Printer JSON_PRINTER = JsonFormat.printer(); - private static final JsonFormat.Printer JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES = JsonFormat.printer().preservingProtoFieldNames(); - private static final boolean PRESERVE_PROTO_FIELD_NAMES = Boolean.parseBoolean(System.getProperty("transport.json.preserve_proto_field_names", System.getenv("TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES"))); - @Override public byte[] encode(TbProtoQueueMsg value) { try { - JsonFormat.Printer printer = PRESERVE_PROTO_FIELD_NAMES ? JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES : JSON_PRINTER; - return printer.print(value.getValue()).getBytes(StandardCharsets.UTF_8); + return JsonFormat.printer().print(value.getValue()).getBytes(StandardCharsets.UTF_8); } catch (InvalidProtocolBufferException e) { throw new RuntimeException(e); } diff --git a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java index 026844b160..6b5bc75306 100644 --- a/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java +++ b/common/transport/lwm2m/src/main/java/org/thingsboard/server/transport/lwm2m/server/store/util/LwM2MClientSerDes.java @@ -53,9 +53,6 @@ import static org.thingsboard.common.util.JacksonUtil.toJsonNode; public class LwM2MClientSerDes { public static final String VALUE = "value"; private static final RegistrationSerDes registrationSerDes = new RegistrationSerDes(); - private static final JsonFormat.Printer JSON_PRINTER = JsonFormat.printer(); - private static final JsonFormat.Printer JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES = JsonFormat.printer().preservingProtoFieldNames(); - private static final boolean PRESERVE_PROTO_FIELD_NAMES = Boolean.parseBoolean(System.getProperty("transport.json.preserve_proto_field_names", System.getenv("TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES"))); @SneakyThrows public static byte[] serialize(LwM2mClient client) { @@ -74,8 +71,7 @@ public class LwM2MClientSerDes { JsonObject sharedAttributes = new JsonObject(); for (Map.Entry entry : client.getSharedAttributes().entrySet()) { - JsonFormat.Printer printer = PRESERVE_PROTO_FIELD_NAMES ? JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES : JSON_PRINTER; - sharedAttributes.addProperty(entry.getKey(), printer.print(entry.getValue())); + sharedAttributes.addProperty(entry.getKey(), JsonFormat.printer().print(entry.getValue())); } o.add("sharedAttributes", sharedAttributes); @@ -88,8 +84,7 @@ public class LwM2MClientSerDes { o.addProperty("state", client.getState().toString()); if (client.getSession() != null) { - JsonFormat.Printer printer = PRESERVE_PROTO_FIELD_NAMES ? JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES : JSON_PRINTER; - o.addProperty("session", printer.print(client.getSession())); + o.addProperty("session", JsonFormat.printer().print(client.getSession())); } if (client.getTenantId() != null) { o.addProperty("tenantId", client.getTenantId().toString()); diff --git a/transport/coap/src/main/resources/tb-coap-transport.yml b/transport/coap/src/main/resources/tb-coap-transport.yml index d9a3dd6e72..7baafbf78d 100644 --- a/transport/coap/src/main/resources/tb-coap-transport.yml +++ b/transport/coap/src/main/resources/tb-coap-transport.yml @@ -158,6 +158,11 @@ transport: type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}" # Maximum allowed string value length when processing Telemetry/Attributes JSON (0 value disables string value length check) max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}" + # Preserve proto field names (e.g., 'current_fw_title') instead of converting to camelCase (e.g., 'currentFwTitle') when processing Protobuf messages. + # When set to 'false' (default), field names are converted to camelCase for backward compatibility. + # When set to 'true', field names are preserved as defined in the .proto schema. + # This affects dynamic Protobuf messages used in device communication. + preserve_proto_field_names: "${TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES:false}" log: # Enable/Disable log of transport messages to telemetry. For example, logging of LwM2M registration update enabled: "${TB_TRANSPORT_LOG_ENABLED:true}" diff --git a/transport/http/src/main/resources/tb-http-transport.yml b/transport/http/src/main/resources/tb-http-transport.yml index 7fe35a57d5..b7c3cf4a1e 100644 --- a/transport/http/src/main/resources/tb-http-transport.yml +++ b/transport/http/src/main/resources/tb-http-transport.yml @@ -189,6 +189,11 @@ transport: type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}" # Maximum allowed string value length when processing Telemetry/Attributes JSON (0 value disables string value length check) max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}" + # Preserve proto field names (e.g., 'current_fw_title') instead of converting to camelCase (e.g., 'currentFwTitle') when processing Protobuf messages. + # When set to 'false' (default), field names are converted to camelCase for backward compatibility. + # When set to 'true', field names are preserved as defined in the .proto schema. + # This affects dynamic Protobuf messages used in device communication. + preserve_proto_field_names: "${TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES:false}" log: # Enable/Disable log of transport messages to telemetry. For example, logging of LwM2M registration update enabled: "${TB_TRANSPORT_LOG_ENABLED:true}" diff --git a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml index 7e609d7a10..d921eb3781 100644 --- a/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml +++ b/transport/lwm2m/src/main/resources/tb-lwm2m-transport.yml @@ -149,6 +149,11 @@ transport: type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:false}" # Maximum allowed string value length when processing Telemetry/Attributes JSON (0 value disables string value length check) max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}" + # Preserve proto field names (e.g., 'current_fw_title') instead of converting to camelCase (e.g., 'currentFwTitle') when processing Protobuf messages. + # When set to 'false' (default), field names are converted to camelCase for backward compatibility. + # When set to 'true', field names are preserved as defined in the .proto schema. + # This affects dynamic Protobuf messages used in device communication. + preserve_proto_field_names: "${TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES:false}" client_side_rpc: # Processing timeout interval of the RPC command on the CLIENT SIDE. Time in milliseconds timeout: "${CLIENT_SIDE_RPC_TIMEOUT:60000}" diff --git a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml index 86e5dc5a5b..a3e2eb5e9b 100644 --- a/transport/mqtt/src/main/resources/tb-mqtt-transport.yml +++ b/transport/mqtt/src/main/resources/tb-mqtt-transport.yml @@ -212,6 +212,11 @@ transport: type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}" # Maximum allowed string value length when processing Telemetry/Attributes JSON (0 value disables string value length check) max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}" + # Preserve proto field names (e.g., 'current_fw_title') instead of converting to camelCase (e.g., 'currentFwTitle') when processing Protobuf messages. + # When set to 'false' (default), field names are converted to camelCase for backward compatibility. + # When set to 'true', field names are preserved as defined in the .proto schema. + # This affects dynamic Protobuf messages used in device communication. + preserve_proto_field_names: "${TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES:false}" log: # Enable/Disable log of transport messages to telemetry. For example, logging of LwM2M registration update enabled: "${TB_TRANSPORT_LOG_ENABLED:true}" diff --git a/transport/snmp/src/main/resources/tb-snmp-transport.yml b/transport/snmp/src/main/resources/tb-snmp-transport.yml index 9dfb9f0b41..274d7e161a 100644 --- a/transport/snmp/src/main/resources/tb-snmp-transport.yml +++ b/transport/snmp/src/main/resources/tb-snmp-transport.yml @@ -170,6 +170,11 @@ transport: type_cast_enabled: "${JSON_TYPE_CAST_ENABLED:true}" # Maximum allowed string value length when processing Telemetry/Attributes JSON (0 value disables string value length check) max_string_value_length: "${JSON_MAX_STRING_VALUE_LENGTH:0}" + # Preserve proto field names (e.g., 'current_fw_title') instead of converting to camelCase (e.g., 'currentFwTitle') when processing Protobuf messages. + # When set to 'false' (default), field names are converted to camelCase for backward compatibility. + # When set to 'true', field names are preserved as defined in the .proto schema. + # This affects dynamic Protobuf messages used in device communication. + preserve_proto_field_names: "${TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES:false}" log: # Enable/Disable log of transport messages to telemetry. For example, logging of LwM2M registration update enabled: "${TB_TRANSPORT_LOG_ENABLED:true}" From 46e64a644f73bd9041c06f2a9dcc3edfd313d726 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 5 Jan 2026 09:45:20 +0000 Subject: [PATCH 7/8] Make tests explicitly set property value instead of depending on environment Co-authored-by: ViacheslavKlimov <56742475+ViacheslavKlimov@users.noreply.github.com> --- .../server/common/data/DynamicProtoUtils.java | 8 +- .../common/data/DynamicProtoUtilsTest.java | 87 +++++++++---------- 2 files changed, 46 insertions(+), 49 deletions(-) diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/DynamicProtoUtils.java b/common/data/src/main/java/org/thingsboard/server/common/data/DynamicProtoUtils.java index 06f79bda3a..cd038463ad 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/DynamicProtoUtils.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/DynamicProtoUtils.java @@ -48,7 +48,7 @@ public class DynamicProtoUtils { private static final JsonFormat.Printer JSON_PRINTER = JsonFormat.printer().includingDefaultValueFields(); private static final JsonFormat.Printer JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES = JsonFormat.printer().preservingProtoFieldNames().includingDefaultValueFields(); - private static final boolean PRESERVE_PROTO_FIELD_NAMES = Boolean.parseBoolean(System.getProperty("transport.json.preserve_proto_field_names", System.getenv("TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES"))); + private static boolean preserveProtoFieldNames = Boolean.parseBoolean(System.getProperty("transport.json.preserve_proto_field_names", System.getenv("TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES"))); public static Descriptors.Descriptor getDescriptor(String protoSchema, String schemaName) { try { @@ -103,7 +103,7 @@ public class DynamicProtoUtils { public static String dynamicMsgToJson(Descriptors.Descriptor descriptor, byte[] payload) throws InvalidProtocolBufferException { DynamicMessage dynamicMessage = DynamicMessage.parseFrom(descriptor, payload); - JsonFormat.Printer printer = PRESERVE_PROTO_FIELD_NAMES ? JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES : JSON_PRINTER; + JsonFormat.Printer printer = preserveProtoFieldNames ? JSON_PRINTER_PRESERVING_PROTO_FIELD_NAMES : JSON_PRINTER; return printer.print(dynamicMessage); } @@ -112,6 +112,10 @@ public class DynamicProtoUtils { return builder.build(); } + static void setPreserveProtoFieldNames(boolean preserve) { + preserveProtoFieldNames = preserve; + } + private static List getMessageTypes(List types) { return types.stream() .filter(typeElement -> typeElement instanceof MessageElement) diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java index 20cf454fc4..763df306d7 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java @@ -19,9 +19,10 @@ import com.github.os72.protobuf.dynamic.DynamicSchema; import com.google.protobuf.Descriptors; import com.google.protobuf.DynamicMessage; import com.squareup.wire.schema.internal.parser.ProtoFileElement; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.junit.jupiter.MockitoExtension; +import org.junit.jupiter.api.parallel.Isolated; import java.util.List; import java.util.Set; @@ -31,9 +32,21 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -@ExtendWith(MockitoExtension.class) +@Isolated("DynamicProtoUtils static settings being modified") public class DynamicProtoUtilsTest { + @BeforeEach + public void before() { + // Restore default state before each test + DynamicProtoUtils.setPreserveProtoFieldNames(false); + } + + @AfterEach + public void after() { + // Restore default state after each test + DynamicProtoUtils.setPreserveProtoFieldNames(false); + } + @Test public void testProtoSchemaWithMessageNestedTypes() throws Exception { String schema = "syntax = \"proto3\";\n" + @@ -169,8 +182,9 @@ public class DynamicProtoUtilsTest { @Test public void testProtoSchemaDefaultBehaviorConvertsToCamelCase() throws Exception { - // Test default behavior when TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES is not set - // Field names should be converted to camelCase for backward compatibility + // Explicitly set to false to test default behavior (camelCase conversion) + DynamicProtoUtils.setPreserveProtoFieldNames(false); + String schema = "syntax = \"proto3\";\n" + "\n" + "package firmware;\n" + @@ -200,33 +214,23 @@ public class DynamicProtoUtilsTest { String json = DynamicProtoUtils.dynamicMsgToJson(firmwareStatusDescriptor, firmwareStatus.toByteArray()); - // Check the actual behavior based on the current flag setting - boolean preserveFieldNames = Boolean.parseBoolean(System.getProperty("transport.json.preserve_proto_field_names", System.getenv("TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES"))); + // Default behavior: field names converted to camelCase + assertTrue("JSON should contain camelCase field 'currentFwTitle'", json.contains("\"currentFwTitle\"")); + assertTrue("JSON should contain camelCase field 'currentFwVersion'", json.contains("\"currentFwVersion\"")); + assertTrue("JSON should contain camelCase field 'fwState'", json.contains("\"fwState\"")); + assertTrue("JSON should contain camelCase field 'targetFwTitle'", json.contains("\"targetFwTitle\"")); + assertTrue("JSON should contain camelCase field 'targetFwVersion'", json.contains("\"targetFwVersion\"")); - if (!preserveFieldNames) { - // Default behavior: field names converted to camelCase - assertTrue("JSON should contain camelCase field 'currentFwTitle'", json.contains("\"currentFwTitle\"")); - assertTrue("JSON should contain camelCase field 'currentFwVersion'", json.contains("\"currentFwVersion\"")); - assertTrue("JSON should contain camelCase field 'fwState'", json.contains("\"fwState\"")); - assertTrue("JSON should contain camelCase field 'targetFwTitle'", json.contains("\"targetFwTitle\"")); - assertTrue("JSON should contain camelCase field 'targetFwVersion'", json.contains("\"targetFwVersion\"")); - - // Verify snake_case versions are NOT present - assertFalse("JSON should NOT contain snake_case field 'current_fw_title'", json.contains("\"current_fw_title\"")); - assertFalse("JSON should NOT contain snake_case field 'fw_state'", json.contains("\"fw_state\"")); - } else { - // This test is designed to verify default behavior, skip if flag is set - // The next test will verify the preserve behavior - assertTrue("This test expects default behavior (camelCase conversion). " + - "Set TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES=false or run without the flag.", - !preserveFieldNames); - } + // Verify snake_case versions are NOT present + assertFalse("JSON should NOT contain snake_case field 'current_fw_title'", json.contains("\"current_fw_title\"")); + assertFalse("JSON should NOT contain snake_case field 'fw_state'", json.contains("\"fw_state\"")); } @Test public void testProtoSchemaPreservesSnakeCaseFieldNamesWhenEnabled() throws Exception { - // Test behavior when TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES is set to true - // Field names should be preserved as defined in .proto schema + // Explicitly set to true to test preserve behavior (snake_case preservation) + DynamicProtoUtils.setPreserveProtoFieldNames(true); + String schema = "syntax = \"proto3\";\n" + "\n" + "package firmware;\n" + @@ -256,27 +260,16 @@ public class DynamicProtoUtilsTest { String json = DynamicProtoUtils.dynamicMsgToJson(firmwareStatusDescriptor, firmwareStatus.toByteArray()); - // Check the actual behavior based on the current flag setting - boolean preserveFieldNames = Boolean.parseBoolean(System.getProperty("transport.json.preserve_proto_field_names", System.getenv("TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES"))); + // When flag is enabled, verify snake_case is preserved + assertTrue("JSON should contain snake_case field 'current_fw_title'", json.contains("\"current_fw_title\"")); + assertTrue("JSON should contain snake_case field 'current_fw_version'", json.contains("\"current_fw_version\"")); + assertTrue("JSON should contain snake_case field 'fw_state'", json.contains("\"fw_state\"")); + assertTrue("JSON should contain snake_case field 'target_fw_title'", json.contains("\"target_fw_title\"")); + assertTrue("JSON should contain snake_case field 'target_fw_version'", json.contains("\"target_fw_version\"")); - if (preserveFieldNames) { - // When flag is enabled, verify snake_case is preserved - assertTrue("JSON should contain snake_case field 'current_fw_title'", json.contains("\"current_fw_title\"")); - assertTrue("JSON should contain snake_case field 'current_fw_version'", json.contains("\"current_fw_version\"")); - assertTrue("JSON should contain snake_case field 'fw_state'", json.contains("\"fw_state\"")); - assertTrue("JSON should contain snake_case field 'target_fw_title'", json.contains("\"target_fw_title\"")); - assertTrue("JSON should contain snake_case field 'target_fw_version'", json.contains("\"target_fw_version\"")); - - // Verify camelCase versions are NOT present - assertFalse("JSON should NOT contain camelCase field 'currentFwTitle'", json.contains("\"currentFwTitle\"")); - assertFalse("JSON should NOT contain camelCase field 'fwState'", json.contains("\"fwState\"")); - } else { - // This test is designed to verify preserve behavior, skip if flag is not set - // Run this test with: TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES=true - assertTrue("This test expects preserve behavior (snake_case preservation). " + - "Set TB_TRANSPORT_JSON_PRESERVE_PROTO_FIELD_NAMES=true to run this test.", - preserveFieldNames); - } + // Verify camelCase versions are NOT present + assertFalse("JSON should NOT contain camelCase field 'currentFwTitle'", json.contains("\"currentFwTitle\"")); + assertFalse("JSON should NOT contain camelCase field 'fwState'", json.contains("\"fwState\"")); } } From d5ced6ad22ad53d78877ce8c24c39a79a966859b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 6 Jan 2026 11:23:17 +0000 Subject: [PATCH 8/8] Fix JUnit 5 assertion argument order in tests (condition first, message second) Co-authored-by: ViacheslavKlimov <56742475+ViacheslavKlimov@users.noreply.github.com> --- .../common/data/DynamicProtoUtilsTest.java | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java b/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java index 763df306d7..241772a044 100644 --- a/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java +++ b/common/data/src/test/java/org/thingsboard/server/common/data/DynamicProtoUtilsTest.java @@ -215,15 +215,15 @@ public class DynamicProtoUtilsTest { String json = DynamicProtoUtils.dynamicMsgToJson(firmwareStatusDescriptor, firmwareStatus.toByteArray()); // Default behavior: field names converted to camelCase - assertTrue("JSON should contain camelCase field 'currentFwTitle'", json.contains("\"currentFwTitle\"")); - assertTrue("JSON should contain camelCase field 'currentFwVersion'", json.contains("\"currentFwVersion\"")); - assertTrue("JSON should contain camelCase field 'fwState'", json.contains("\"fwState\"")); - assertTrue("JSON should contain camelCase field 'targetFwTitle'", json.contains("\"targetFwTitle\"")); - assertTrue("JSON should contain camelCase field 'targetFwVersion'", json.contains("\"targetFwVersion\"")); + assertTrue(json.contains("\"currentFwTitle\""), "JSON should contain camelCase field 'currentFwTitle'"); + assertTrue(json.contains("\"currentFwVersion\""), "JSON should contain camelCase field 'currentFwVersion'"); + assertTrue(json.contains("\"fwState\""), "JSON should contain camelCase field 'fwState'"); + assertTrue(json.contains("\"targetFwTitle\""), "JSON should contain camelCase field 'targetFwTitle'"); + assertTrue(json.contains("\"targetFwVersion\""), "JSON should contain camelCase field 'targetFwVersion'"); // Verify snake_case versions are NOT present - assertFalse("JSON should NOT contain snake_case field 'current_fw_title'", json.contains("\"current_fw_title\"")); - assertFalse("JSON should NOT contain snake_case field 'fw_state'", json.contains("\"fw_state\"")); + assertFalse(json.contains("\"current_fw_title\""), "JSON should NOT contain snake_case field 'current_fw_title'"); + assertFalse(json.contains("\"fw_state\""), "JSON should NOT contain snake_case field 'fw_state'"); } @Test @@ -261,15 +261,15 @@ public class DynamicProtoUtilsTest { String json = DynamicProtoUtils.dynamicMsgToJson(firmwareStatusDescriptor, firmwareStatus.toByteArray()); // When flag is enabled, verify snake_case is preserved - assertTrue("JSON should contain snake_case field 'current_fw_title'", json.contains("\"current_fw_title\"")); - assertTrue("JSON should contain snake_case field 'current_fw_version'", json.contains("\"current_fw_version\"")); - assertTrue("JSON should contain snake_case field 'fw_state'", json.contains("\"fw_state\"")); - assertTrue("JSON should contain snake_case field 'target_fw_title'", json.contains("\"target_fw_title\"")); - assertTrue("JSON should contain snake_case field 'target_fw_version'", json.contains("\"target_fw_version\"")); + assertTrue(json.contains("\"current_fw_title\""), "JSON should contain snake_case field 'current_fw_title'"); + assertTrue(json.contains("\"current_fw_version\""), "JSON should contain snake_case field 'current_fw_version'"); + assertTrue(json.contains("\"fw_state\""), "JSON should contain snake_case field 'fw_state'"); + assertTrue(json.contains("\"target_fw_title\""), "JSON should contain snake_case field 'target_fw_title'"); + assertTrue(json.contains("\"target_fw_version\""), "JSON should contain snake_case field 'target_fw_version'"); // Verify camelCase versions are NOT present - assertFalse("JSON should NOT contain camelCase field 'currentFwTitle'", json.contains("\"currentFwTitle\"")); - assertFalse("JSON should NOT contain camelCase field 'fwState'", json.contains("\"fwState\"")); + assertFalse(json.contains("\"currentFwTitle\""), "JSON should NOT contain camelCase field 'currentFwTitle'"); + assertFalse(json.contains("\"fwState\""), "JSON should NOT contain camelCase field 'fwState'"); } }