diff --git a/ReleaseNotes.md b/ReleaseNotes.md
index 06cb295b96a7fdee4b3bbca96695676a25f6a650..21d83edad22caca172189d4254ff49a32426a682 100644
--- a/ReleaseNotes.md
+++ b/ReleaseNotes.md
@@ -20,6 +20,7 @@
 - cta/CTA#683 - Fix problems with field consistency in json logging
 - cta/CTA#688 - Fix tapeserver umask to allow directory creation in POSIX filesystems
 - cta/CTA#693 - Fix tapeserver tool regressions
+- cta/CTA#704 - Fix special character encoding in json logging
 
 ### Continuous Integration
 - cta/CTA#615 - Going to xrdfs xattr API for EOS5 extended attribute tests (EOS >= 5.2.17)
diff --git a/common/log/JsonTest.cpp b/common/log/JsonTest.cpp
index a18f349fcee2325db8ae9f3c8a328880fb95438c..cf345a697b11f687162c123ecbe757d1d3e39e45 100644
--- a/common/log/JsonTest.cpp
+++ b/common/log/JsonTest.cpp
@@ -151,4 +151,49 @@ TEST_F(cta_log_JsonTest, testJsonPrinting) {
   ASSERT_EQ(0, jObjB.jsonGetValueProbe<int64_t>("ParamsB_null"));
 }
 
+TEST_F(cta_log_JsonTest, testJsonStringEscape) {
+  using namespace cta::log;
+
+  // Prepare the logger for inspection
+  StringLogger logger("dummy", "cta_log_JsonTest", cta::log::DEBUG);
+  LogContext logContext(logger);
+  logger.setLogFormat("json");
+
+  {
+    cta::log::ScopedParamContainer paramsA(logContext);
+    paramsA.add("key_\"", "value_\"");
+    paramsA.add("key_\\", "value_\\");
+    paramsA.add("key_\b", "value_\b");
+    paramsA.add("key_\n", "value_\n");
+    paramsA.add("key_\f", "value_\f");
+    paramsA.add("key_\r", "value_\r");
+    paramsA.add("key_\t", "value_\t");
+    paramsA.add("key_\x00", "value_\x00");
+    paramsA.add("key_\x1f", "value_\x1f");
+    paramsA.add("key_\x20", "value_\x20"); //This is a whitespace character
+    logContext.log(INFO, "Testing escaped values");
+  }
+
+  std::string logLine = logger.getLog();
+
+  JSONCObjectProbe jObj;
+  jObj.buildFromJSON(logLine);
+
+  // Check that JSON is parsed correctly
+  ASSERT_NO_THROW(jObj.getJSON());
+
+  // Check expected keys and values
+  // Strings should be converted back to cpp with the escape characters correctly decoded
+  ASSERT_EQ("value_\"", jObj.jsonGetValueProbe<std::string>("key_\""));
+  ASSERT_EQ("value_\\", jObj.jsonGetValueProbe<std::string>("key_\\"));
+  ASSERT_EQ("value_\b", jObj.jsonGetValueProbe<std::string>("key_\b"));
+  ASSERT_EQ("value_\n", jObj.jsonGetValueProbe<std::string>("key_\n"));
+  ASSERT_EQ("value_\f", jObj.jsonGetValueProbe<std::string>("key_\f"));
+  ASSERT_EQ("value_\r", jObj.jsonGetValueProbe<std::string>("key_\r"));
+  ASSERT_EQ("value_\t", jObj.jsonGetValueProbe<std::string>("key_\t"));
+  ASSERT_EQ("value_\x00", jObj.jsonGetValueProbe<std::string>("key_\x00"));
+  ASSERT_EQ("value_\x1f", jObj.jsonGetValueProbe<std::string>("key_\x1f"));
+  ASSERT_EQ("value_ ", jObj.jsonGetValueProbe<std::string>("key_ "));
+}
+
 } // namespace unitTests
diff --git a/common/log/Param.cpp b/common/log/Param.cpp
index 5875624eb38dcaad34349495cb104d882ef51aa7..4111c3c1fc71a3795e972bde57f1a65c8c4513b4 100644
--- a/common/log/Param.cpp
+++ b/common/log/Param.cpp
@@ -19,6 +19,7 @@
 
 #include <iostream>
 #include <iomanip>
+#include <algorithm>
 
 namespace cta::log {
 
@@ -61,14 +62,14 @@ std::string Param::getValueStr() const noexcept {
 //------------------------------------------------------------------------------
 std::string Param::getKeyValueJSON() const noexcept {
   std::ostringstream oss;
-  oss << "\"" << m_name << "\":";
+  oss << "\"" << stringFormattingJSON(m_name) << "\":";
   if (m_value.has_value()) {
     std::visit([&oss](auto &&arg) {
       using T = std::decay_t<decltype(arg)>;
       if constexpr (std::is_same_v<T, bool>) {
         oss << (arg ? "true" : "false");
       } else if constexpr (std::is_same_v<T, std::string>) {
-        oss << "\"" << arg << "\"";
+        oss << "\"" << stringFormattingJSON(arg) << "\"";
       } else if constexpr (std::is_integral_v<T>) {
         oss << arg;
       } else if constexpr (std::is_floating_point_v<T>) {
@@ -88,4 +89,35 @@ void Param::setValue<ParamValType>(const ParamValType& value) noexcept {
   m_value = value;
 }
 
+//------------------------------------------------------------------------------
+// stringFormattingJSON nested class
+//------------------------------------------------------------------------------
+Param::stringFormattingJSON::stringFormattingJSON(const std::string& str) : m_value(str) {}
+
+//------------------------------------------------------------------------------
+// stringFormattingJSON << operator overload
+//------------------------------------------------------------------------------
+std::ostream& operator<<(std::ostream& oss, const Param::stringFormattingJSON& fp) {
+  std::ostringstream oss_tmp;
+  for (char c : fp.m_value) {
+    switch (c) {
+    case '\"': oss_tmp << R"(\")"; break;
+    case '\\': oss_tmp << R"(\\)"; break;
+    case '\b': oss_tmp << R"(\b)"; break;
+    case '\f': oss_tmp << R"(\f)"; break;
+    case '\n': oss_tmp << R"(\n)"; break;
+    case '\r': oss_tmp << R"(\r)"; break;
+    case '\t': oss_tmp << R"(\t)"; break;
+    default:
+      if ('\x00' <= c && c <= '\x1f') {
+        oss_tmp << R"(\u)" << std::hex << std::setw(4) << std::setfill('0') << static_cast<unsigned int>(c);
+      } else {
+        oss_tmp << c;
+      }
+    }
+  }
+  oss << oss_tmp.str();
+  return oss;
+}
+
 } // namespace cta::log
diff --git a/common/log/Param.hpp b/common/log/Param.hpp
index 5d6ead394f36bc7aad04d4c056d190f54c581d40..ad0f50261e01df7a99feb7505fdf1a180be1bc65 100644
--- a/common/log/Param.hpp
+++ b/common/log/Param.hpp
@@ -149,6 +149,19 @@ protected:
    */
   ParamValType m_value;
 
+  /**
+   * Helper class to format string values in JSON
+   */
+  class stringFormattingJSON {
+  public:
+    explicit stringFormattingJSON(const std::string& str);
+    friend std::ostream& operator<<(std::ostream& oss, const stringFormattingJSON& fp);
+  private:
+    const std::string & m_value;
+  };
+
+  friend std::ostream& operator<<(std::ostream& oss, const Param::stringFormattingJSON& fp);
+
   /**
    * Helper class to format floating-point values
    */