diff --git a/include/cgimap/api06/changeset_upload/osmchange_json_input_format.hpp b/include/cgimap/api06/changeset_upload/osmchange_json_input_format.hpp
new file mode 100644
index 00000000..70a9140e
--- /dev/null
+++ b/include/cgimap/api06/changeset_upload/osmchange_json_input_format.hpp
@@ -0,0 +1,325 @@
+/**
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * This file is part of openstreetmap-cgimap (https://github.com/zerebubuth/openstreetmap-cgimap/).
+ *
+ * Copyright (C) 2009-2023 by the CGImap developer community.
+ * For a full list of authors see the git log.
+ */
+
+#ifndef OSMCHANGE_JSON_INPUT_FORMAT_HPP
+#define OSMCHANGE_JSON_INPUT_FORMAT_HPP
+
+#include "cgimap/api06/changeset_upload/node.hpp"
+#include "cgimap/api06/changeset_upload/osmobject.hpp"
+#include "cgimap/api06/changeset_upload/parser_callback.hpp"
+#include "cgimap/api06/changeset_upload/relation.hpp"
+#include "cgimap/api06/changeset_upload/way.hpp"
+#include "cgimap/types.hpp"
+
+#include "sjparser/sjparser.h"
+
+#include <fmt/core.h>
+
+#include <cassert>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+
+namespace api06 {
+
+using SJParser::Array;
+using SJParser::Member;
+using SJParser::Object;
+using SJParser::Parser;
+using SJParser::Presence;
+using SJParser::SArray;
+using SJParser::SAutoObject;
+using SJParser::SMap;
+using SJParser::Value;
+using SJParser::Reaction;
+using SJParser::ObjectOptions;
+
+using std::placeholders::_1;
+
+class OSMChangeJSONParserFormat {
+
+  static auto getMemberParser() {
+    return SAutoObject{std::tuple{Member{"type", Value<std::string>{}},
+                                  Member{"ref", Value<int64_t>{}},
+                                  Member{"role", Value<std::string>{}, Presence::Optional, ""}},
+                                  ObjectOptions{Reaction::Ignore}
+                                  };
+  }
+
+  template <typename ElementParserCallback = std::nullptr_t>
+  static auto getElementsParser(ElementParserCallback element_parser_callback = nullptr) {
+    return Object{
+          std::tuple{
+            Member{"type", Value<std::string>{}},
+            Member{"action", Value<std::string>{}},
+            Member{"if-unused", Value<bool>{}, Presence::Optional, false},
+            Member{"id", Value<int64_t>{}},
+            Member{"lat", Value<double>{}, Presence::Optional},
+            Member{"lon", Value<double>{}, Presence::Optional},
+            Member{"version", Value<int64_t>{}, Presence::Optional},
+            Member{"changeset", Value<int64_t>{}},
+            Member{"tags", SMap{Value<std::string>{}}, Presence::Optional},
+            Member{"nodes", SArray{Value<int64_t>{}}, Presence::Optional},
+            Member{"members", SArray{getMemberParser()}, Presence::Optional}
+          },
+        ObjectOptions{Reaction::Ignore},
+        element_parser_callback};
+  }
+
+  template <typename ElementParserCallback = std::nullptr_t>
+  static auto getMainParser(ElementParserCallback element_parser_callback = nullptr) {
+    return Parser{
+       Object{
+        std::tuple{
+          Member{"version", Value<std::string>{}, Presence::Optional},
+          Member{"generator", Value<std::string>{}, Presence::Optional},
+          Member{"osmChange",  Array{getElementsParser(element_parser_callback)}}
+        },ObjectOptions{Reaction::Ignore}}};
+  }
+
+  friend class OSMChangeJSONParser;
+};
+
+class OSMChangeJSONParser {
+
+public:
+  explicit OSMChangeJSONParser(Parser_Callback& callback)
+      : m_callback(callback) { }
+
+  OSMChangeJSONParser(const OSMChangeJSONParser &) = delete;
+  OSMChangeJSONParser &operator=(const OSMChangeJSONParser &) = delete;
+
+  OSMChangeJSONParser(OSMChangeJSONParser &&) = delete;
+  OSMChangeJSONParser &operator=(OSMChangeJSONParser &&) = delete;
+
+  void process_message(const std::string &data) {
+
+    try {
+      m_callback.start_document();
+      _parser.parse(data);
+      _parser.finish();
+
+      if (_parser.parser().isEmpty()) {
+        throw payload_error("Empty JSON payload");
+      }
+
+      if (element_count == 0) {
+        throw payload_error("osmChange array is empty");
+      }
+
+      m_callback.end_document();
+    } catch (const std::exception& e) {
+      throw http::bad_request(e.what());    // rethrow JSON parser error as HTTP 400 Bad request
+    }
+  }
+
+private:
+
+  using ElementsParser = decltype(api06::OSMChangeJSONParserFormat::getElementsParser());
+  using MainParser = decltype(api06::OSMChangeJSONParserFormat::getMainParser());
+
+  MainParser _parser{api06::OSMChangeJSONParserFormat::getMainParser(std::bind(&api06::OSMChangeJSONParser::process_element, this, _1))};
+
+  // OSM element callback
+  bool process_element(ElementsParser &parser) {
+
+    element_count++;
+
+    // process action
+    process_action(parser);
+
+    // process if-unused flag for delete action
+    process_if_unused(parser);
+
+    // process type (node, way, relation)
+    process_type(parser);
+
+    return true;
+  }
+
+  void process_action(ElementsParser &parser) {
+
+    const std::string& action = parser.get<1>();
+
+    if (action == "create") {
+      m_operation = operation::op_create;
+    } else if (action == "modify") {
+      m_operation = operation::op_modify;
+    } else if (action == "delete") {
+      m_operation = operation::op_delete;
+    } else {
+      throw payload_error{fmt::format("Unknown action {}, choices are create, modify, delete", action)};
+    }
+  }
+
+  void process_if_unused(ElementsParser &parser) {
+
+    if (m_operation == operation::op_delete) {
+      m_if_unused = false;
+      if (parser.parser<2>().isSet()) {
+        m_if_unused = parser.get<2>();
+      }
+    }
+  }
+
+  void process_type(ElementsParser &parser) {
+
+    const std::string& type = parser.get<0>();
+
+    if (type == "node") {
+      process_node(parser);
+    } else if (type == "way") {
+      process_way(parser);
+    } else if (type == "relation") {
+      process_relation(parser);
+    } else {
+      throw payload_error{fmt::format("Unknown element {}, expecting node, way or relation", type)};
+    }
+  }
+
+  void process_node(ElementsParser& parser) {
+
+    Node node;
+    init_object(node, parser);
+
+    if (parser.parser<4>().isSet()) {
+      node.set_lat(parser.get<4>());
+    }
+
+    if (parser.parser<5>().isSet()) {
+      node.set_lon(parser.get<5>());
+    }
+
+    process_tags(node, parser);
+
+    if (!node.is_valid(m_operation)) {
+      throw payload_error{fmt::format("{} does not include all mandatory fields", node.to_string())};
+    }
+
+    m_callback.process_node(node, m_operation, m_if_unused);
+  }
+
+  void process_way(ElementsParser& parser) {
+
+    Way way;
+    init_object(way, parser);
+
+    // adding way nodes
+    if (parser.parser<9>().isSet()) {
+      for (const auto& value : parser.get<9>()) {
+          way.add_way_node(value);
+      }
+    }
+
+    process_tags(way, parser);
+
+    if (!way.is_valid(m_operation)) {
+      throw payload_error{fmt::format("{} does not include all mandatory fields", way.to_string())};
+    }
+
+    m_callback.process_way(way, m_operation, m_if_unused);
+  }
+
+  void process_relation(ElementsParser& parser) {
+
+    Relation relation;
+    init_object(relation, parser);
+
+    process_relation_members(relation, parser);
+
+    process_tags(relation, parser);
+
+    if (!relation.is_valid(m_operation)) {
+      throw payload_error{fmt::format("{} does not include all mandatory fields", relation.to_string())};
+    }
+
+    m_callback.process_relation(relation, m_operation, m_if_unused);
+  }
+
+  void process_relation_members(Relation &relation, ElementsParser& parser) {
+
+    if (!parser.parser<10>().isSet()) {
+      return;
+    }
+
+    for (auto &mbr : parser.get<10>()) {
+      const auto& [type, ref, role] = mbr;
+
+      RelationMember member;
+      member.set_type(type);
+      member.set_ref(ref);
+      member.set_role(role);
+
+      if (!member.is_valid()) {
+        throw payload_error{fmt::format("Missing mandatory field on relation member in {}", relation.to_string()) };
+      }
+      relation.add_member(member);
+    }
+  }
+
+  void process_tags(OSMObject &o, ElementsParser& parser) {
+
+    if (parser.parser<8>().isSet()) {
+      for (const auto &tag : parser.get<8>()) {
+         o.add_tag(tag.first, tag.second);
+      }
+    }
+  }
+
+  void init_object(OSMObject &object, ElementsParser& parser) {
+
+    // id
+    object.set_id(parser.get<3>());
+
+    // version
+    if (parser.parser<6>().isSet()) {
+      object.set_version(parser.get<6>());
+    }
+
+    // changeset
+    if (parser.parser<7>().isSet()) {
+      object.set_changeset(parser.get<7>());
+    }
+
+    // TODO: not needed, handled by sjparser
+    if (!object.has_id()) {
+     	throw payload_error{ "Mandatory field id missing in object" };
+    }
+
+    if (!object.has_changeset()) {
+      throw payload_error{fmt::format("Changeset id is missing for {}", object.to_string()) };
+    }
+
+    if (m_operation == operation::op_create) {
+      // we always override version number for create operations (they are not
+      // mandatory)
+      object.set_version(0u);
+    } else if (m_operation == operation::op_delete ||
+               m_operation == operation::op_modify) {
+      // objects for other operations must have a positive version number
+      if (!object.has_version()) {
+        throw payload_error{fmt::format("Version is required when updating {}", object.to_string()) };
+      }
+      if (object.version() < 1) {
+        throw payload_error{ fmt::format("Invalid version number {} in {}", object.version(), object.to_string()) };
+      }
+    }
+  }
+
+  operation m_operation = operation::op_undefined;
+  Parser_Callback& m_callback;
+  bool m_if_unused = false;
+  int element_count = 0;
+};
+
+} // namespace api06
+
+#endif // OSMCHANGE_JSON_INPUT_FORMAT_HPP
diff --git a/include/cgimap/json_formatter.hpp b/include/cgimap/json_formatter.hpp
index 10ea1bf3..6ce1249b 100644
--- a/include/cgimap/json_formatter.hpp
+++ b/include/cgimap/json_formatter.hpp
@@ -42,6 +42,8 @@ class json_formatter : public output_formatter {
   void start_changeset(bool) override;
   void end_changeset(bool) override;
 
+  void start_diffresult() override;
+  void end_diffresult() override;
   void start_action(action_type type) override;
   void end_action(action_type type) override;
   void error(const std::exception &e) override;
@@ -63,6 +65,7 @@ class json_formatter : public output_formatter {
 				      const osm_nwr_signed_id_t old_id,
 				      const osm_nwr_id_t new_id,
 				      const osm_version_t new_version) override;
+
   void write_diffresult_delete(const element_type elem,
 			       const osm_nwr_signed_id_t old_id) override;
 
diff --git a/include/cgimap/osm_diffresult_responder.hpp b/include/cgimap/osm_diffresult_responder.hpp
index 8a44ed6a..075337d3 100644
--- a/include/cgimap/osm_diffresult_responder.hpp
+++ b/include/cgimap/osm_diffresult_responder.hpp
@@ -27,6 +27,8 @@ class osm_diffresult_responder : public osm_responder {
 
   ~osm_diffresult_responder() override;
 
+  // lists the standard types that OSM format can respond in
+  std::vector<mime::type> types_available() const override;
 
   void write(output_formatter& f,
              const std::string &generator,
diff --git a/include/cgimap/output_formatter.hpp b/include/cgimap/output_formatter.hpp
index d7116272..3e419f53 100644
--- a/include/cgimap/output_formatter.hpp
+++ b/include/cgimap/output_formatter.hpp
@@ -56,6 +56,23 @@ T element_type_name(element_type elt) noexcept {
   return "";
 }
 
+template <typename T = const char*>
+T action_type_name(action_type action) noexcept {
+
+  switch (action) {
+  case action_type::create:
+    return "create";
+    break;
+  case action_type::modify:
+    return "modify";
+    break;
+  case action_type::del:
+    return "delete";
+    break;
+  }
+  return "";
+}
+
 } // anonymous namespace
 
 struct element_info {
@@ -216,6 +233,11 @@ struct output_formatter {
 
   virtual void end_changeset(bool) = 0;
 
+  // marks the beginning of diffResult response processing
+  virtual void start_diffresult() = 0;
+
+  virtual void end_diffresult() = 0;
+
   // TODO: document me.
   virtual void start_action(action_type type) = 0;
   virtual void end_action(action_type type) = 0;
diff --git a/include/cgimap/text_formatter.hpp b/include/cgimap/text_formatter.hpp
index bffc77ab..ad69fc7d 100644
--- a/include/cgimap/text_formatter.hpp
+++ b/include/cgimap/text_formatter.hpp
@@ -38,6 +38,8 @@ class text_formatter : public output_formatter {
   void start_changeset(bool) override;
   void end_changeset(bool) override;
 
+  void start_diffresult() override;
+  void end_diffresult() override;
   void start_action(action_type type) override;
   void end_action(action_type type) override;
   void error(const std::exception &e) override;
diff --git a/include/cgimap/xml_formatter.hpp b/include/cgimap/xml_formatter.hpp
index 5cd05068..9fb4a260 100644
--- a/include/cgimap/xml_formatter.hpp
+++ b/include/cgimap/xml_formatter.hpp
@@ -39,6 +39,9 @@ class xml_formatter : public output_formatter {
   void start_changeset(bool) override;
   void end_changeset(bool) override;
 
+  void start_diffresult() override;
+  void end_diffresult() override;
+
   void start_action(action_type type) override;
   void end_action(action_type type) override;
   void error(const std::exception &e) override;
diff --git a/src/api06/changeset_upload_handler.cpp b/src/api06/changeset_upload_handler.cpp
index b15ff696..fc107939 100644
--- a/src/api06/changeset_upload_handler.cpp
+++ b/src/api06/changeset_upload_handler.cpp
@@ -14,6 +14,7 @@
 
 #include "cgimap/api06/changeset_upload/osmchange_handler.hpp"
 #include "cgimap/api06/changeset_upload/osmchange_xml_input_format.hpp"
+#include "cgimap/api06/changeset_upload/osmchange_json_input_format.hpp"
 #include "cgimap/api06/changeset_upload/osmchange_tracking.hpp"
 #include "cgimap/api06/changeset_upload_handler.hpp"
 #include "cgimap/backend/apidb/changeset_upload/changeset_updater.hpp"
@@ -58,6 +59,9 @@ changeset_upload_responder::changeset_upload_responder(mime::type mt,
   if (mt != mime::type::application_json) {
     OSMChangeXMLParser(handler).process_message(payload);
   }
+  else {
+    OSMChangeJSONParser(handler).process_message(payload);
+  }
 
   // store diffresult for output handling in class osm_diffresult_responder
   m_diffresult = change_tracking.assemble_diffresult();
diff --git a/src/json_formatter.cpp b/src/json_formatter.cpp
index 18b8d15d..950b1413 100644
--- a/src/json_formatter.cpp
+++ b/src/json_formatter.cpp
@@ -89,6 +89,16 @@ void json_formatter::end_document() {
   writer->end_object();
 }
 
+void json_formatter::start_diffresult() {
+
+  writer->object_key("diffResult");
+  writer->start_array();
+}
+
+void json_formatter::end_diffresult() {
+  writer->end_array();
+}
+
 void json_formatter::start_action(action_type type) {
 }
 
@@ -248,28 +258,22 @@ void json_formatter::write_diffresult_create_modify(const element_type elem,
                                             const osm_version_t new_version)
 {
 
-//  writer->start_object();
-//  writer->object_key("type");
-//  writer->entry_string(element_type_name(elem));
-//  writer->object_key("old_id");
-//  writer->entry_int(old_id);
-//  writer->object_key("new_id");
-//  writer->entry_int(new_id);
-//  writer->object_key("new_version");
-//  writer->entry_int(new_version);
-//  writer->end_object();
+  writer->start_object();
+  writer->property("type", element_type_name(elem));
+  writer->property("old_id", old_id);
+  writer->property("new_id", new_id);
+  writer->property("new_version", new_version);
+  writer->end_object();
 }
 
 
 void json_formatter::write_diffresult_delete(const element_type elem,
                                             const osm_nwr_signed_id_t old_id)
 {
-//  writer->start_object();
-//  writer->object_key("type");
-//  writer->entry_string(element_type_name(elem));
-//  writer->object_key("old_id");
-//  writer->entry_int(old_id);
-//  writer->end_object();
+  writer->start_object();
+  writer->property("type", element_type_name(elem));
+  writer->property("old_id", old_id);
+  writer->end_object();
 }
 
 void json_formatter::flush() { writer->flush(); }
diff --git a/src/osm_diffresult_responder.cpp b/src/osm_diffresult_responder.cpp
index 821d7c0e..ab1cac43 100644
--- a/src/osm_diffresult_responder.cpp
+++ b/src/osm_diffresult_responder.cpp
@@ -32,7 +32,6 @@ namespace {
 
     throw std::runtime_error("Unhandled object_type in as_elem_type.");
   }
-
 }
 
 osm_diffresult_responder::osm_diffresult_responder(mime::type mt)
@@ -40,6 +39,13 @@ osm_diffresult_responder::osm_diffresult_responder(mime::type mt)
 
 osm_diffresult_responder::~osm_diffresult_responder() = default;
 
+std::vector<mime::type> osm_diffresult_responder::types_available() const {
+  std::vector<mime::type> types;
+  types.push_back(mime::type::application_xml);
+  types.push_back(mime::type::application_json);
+  return types;
+}
+
 void osm_diffresult_responder::write(output_formatter& fmt,
                                      const std::string &generator,
                                      const std::chrono::system_clock::time_point &) {
@@ -48,6 +54,8 @@ void osm_diffresult_responder::write(output_formatter& fmt,
   try {
     fmt.start_document(generator, "diffResult");
 
+    fmt.start_diffresult();
+
     // Iterate over all elements in the sequence defined in the osmChange
     // message
     for (const auto &item : m_diffresult) {
@@ -77,6 +85,8 @@ void osm_diffresult_responder::write(output_formatter& fmt,
       }
     }
 
+    fmt.end_diffresult();
+
   } catch (const std::exception &e) {
     logger::message(fmt::format("Caught error in osm_diffresult_responder: {}",
                         e.what()));
diff --git a/src/osmchange_responder.cpp b/src/osmchange_responder.cpp
index 757185af..15d6ae1e 100644
--- a/src/osmchange_responder.cpp
+++ b/src/osmchange_responder.cpp
@@ -185,6 +185,16 @@ struct sorting_formatter : public output_formatter {
     throw std::runtime_error("Unexpected call to end_action.");
   }
 
+  void start_diffresult() override {
+    // this shouldn't be called here
+    throw std::runtime_error("Unexpected call to start_diffresult.");
+  }
+
+  void end_diffresult() override {
+    // this shouldn't be called here
+    throw std::runtime_error("Unexpected call to end_diffresult.");
+  }
+
   void write(output_formatter &fmt) {
     std::sort(m_elements.begin(), m_elements.end());
     for (const auto &e : m_elements) {
diff --git a/src/text_formatter.cpp b/src/text_formatter.cpp
index c395566f..47af434c 100644
--- a/src/text_formatter.cpp
+++ b/src/text_formatter.cpp
@@ -47,6 +47,15 @@ void text_formatter::end_changeset(bool) {
   // nothing needed here
 }
 
+void text_formatter::start_diffresult() {
+  // nothing needed here
+}
+
+void text_formatter::end_diffresult() {
+  // nothing needed here
+}
+
+
 void text_formatter::start_action(action_type type) {
   // nothing needed here
 }
diff --git a/src/xml_formatter.cpp b/src/xml_formatter.cpp
index 79da53e2..5ecca633 100644
--- a/src/xml_formatter.cpp
+++ b/src/xml_formatter.cpp
@@ -60,18 +60,17 @@ void xml_formatter::end_changeset(bool) {
   // nothing to do for xml
 }
 
+void xml_formatter::start_diffresult() {
+  // not needed in case of xml
+}
+
+void xml_formatter::end_diffresult() {
+  // not needed in case of xml
+}
+
+
 void xml_formatter::start_action(action_type type) {
-  switch (type) {
-  case action_type::create:
-    writer->start("create");
-    break;
-  case action_type::modify:
-    writer->start("modify");
-    break;
-  case action_type::del:
-    writer->start("delete");
-    break;
-  }
+  writer->start(action_type_name(type));
 }
 
 void xml_formatter::end_action(action_type type) {
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 850d33c3..4046d93e 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -124,7 +124,20 @@ if(BUILD_TESTING)
     add_test(NAME test_parse_osmchange_xml_input
         COMMAND test_parse_osmchange_xml_input)
 
+    #################################
+    # test_parse_osmchange_json_input
+    #################################
+    add_executable(test_parse_osmchange_json_input
+        test_parse_osmchange_json_input.cpp)
 
+    target_link_libraries(test_parse_osmchange_json_input
+        cgimap_common_compiler_options
+        cgimap_core
+        Boost::program_options
+        catch2)
+
+    add_test(NAME test_parse_osmchange_json_input
+        COMMAND test_parse_osmchange_json_input)
 
     ############################
     # test_parse_changeset_input
@@ -275,6 +288,7 @@ if(BUILD_TESTING)
                            test_http
                            test_parse_time
                            test_parse_osmchange_xml_input
+                           test_parse_osmchange_json_input                           
                            test_parse_changeset_input)
 
       add_dependencies(check test_apidb_backend_nodes
diff --git a/test/test_apidb_backend_changeset_uploads.cpp b/test/test_apidb_backend_changeset_uploads.cpp
index 86d11f50..4cf1be61 100644
--- a/test/test_apidb_backend_changeset_uploads.cpp
+++ b/test/test_apidb_backend_changeset_uploads.cpp
@@ -2454,6 +2454,66 @@ TEST_CASE_METHOD( DatabaseTestsFixture, "test_osmchange_end_to_end", "[changeset
     REQUIRE(req.response_status() == 200);
   }
 
+  SECTION("JSON upload")
+  {
+    std::string payload = R"(
+        {
+          "version": "0.6",
+          "generator": "demo",
+          "osmChange": [
+            {
+              "type": "node",
+              "action": "create",
+              "id": -1,
+              "lat": 42,
+              "lon": 13,
+              "changeset": 1
+            },
+            {
+              "type": "node",
+              "action": "modify",
+              "id": -1,
+              "version": 1,
+              "lat": 42.7957187,
+              "lon": 13.5690032,
+              "changeset": 1,
+              "tags": {
+                "man_made": "mast",
+                "name": "Monte Piselli - San Giacomo"
+              }
+            }
+          ]
+        }
+      )";
+
+    req.set_header("REQUEST_URI", "/api/0.6/changeset/1/upload.json");
+    req.set_payload(payload);
+
+    // execute the request
+    process_request(req, limiter, generator, route, *sel_factory, upd_factory.get());
+
+    CAPTURE(req.body().str());
+
+    REQUIRE(req.response_status() == 200);
+
+    SECTION("Validate diffResult in JSON format")
+    {
+      pt::ptree act_tree;
+      std::stringstream ss(req.body().str());
+      pt::read_json(ss, act_tree);
+
+      auto diffResult = act_tree.get_child("diffResult");
+      int version = 1;
+      for (auto & entry : diffResult) {
+        REQUIRE(entry.second.get<std::string>("type") == "node");
+        REQUIRE(entry.second.get<int64_t>("old_id") == -1);
+        REQUIRE(entry.second.get<int64_t>("new_id") > 0);
+        REQUIRE(entry.second.get<int64_t>("new_version") == version);
+        version++;
+      }
+    }
+  }
+
 }
 
 
diff --git a/test/test_formatter.cpp b/test/test_formatter.cpp
index 39bc2749..50806444 100644
--- a/test/test_formatter.cpp
+++ b/test/test_formatter.cpp
@@ -152,6 +152,12 @@ void test_formatter::start_document(
 void test_formatter::end_document() {
 }
 
+void test_formatter::start_diffresult() {
+}
+
+void test_formatter::end_diffresult() {
+}
+
 void test_formatter::write_bounds(const bbox &bounds) {
 }
 
diff --git a/test/test_formatter.hpp b/test/test_formatter.hpp
index 47454354..df356616 100644
--- a/test/test_formatter.hpp
+++ b/test/test_formatter.hpp
@@ -89,6 +89,8 @@ struct test_formatter : public output_formatter {
   [[nodiscard]] mime::type mime_type() const override;
   void start_document(const std::string &generator, const std::string &root_name) override;
   void end_document() override;
+  void start_diffresult() override;
+  void end_diffresult() override;
   void write_bounds(const bbox &bounds) override;
   void start_element() override;
   void end_element() override;
diff --git a/test/test_parse_osmchange_json_input.cpp b/test/test_parse_osmchange_json_input.cpp
new file mode 100644
index 00000000..3c7d46a2
--- /dev/null
+++ b/test/test_parse_osmchange_json_input.cpp
@@ -0,0 +1,867 @@
+/**
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
+ * This file is part of openstreetmap-cgimap (https://github.com/zerebubuth/openstreetmap-cgimap/).
+ *
+ * Copyright (C) 2009-2023 by the CGImap developer community.
+ * For a full list of authors see the git log.
+ */
+
+
+#include "cgimap/options.hpp"
+#include "cgimap/api06/changeset_upload/osmchange_json_input_format.hpp"
+#include "cgimap/api06/changeset_upload/parser_callback.hpp"
+#include "cgimap/util.hpp"
+#include "cgimap/http.hpp"
+
+#include <iostream>
+#include <list>
+#include <memory>
+#include <sstream>
+#include <stdexcept>
+
+#define CATCH_CONFIG_MAIN
+#include <catch2/catch.hpp>
+
+class Test_Parser_Callback : public api06::Parser_Callback {
+
+public:
+  Test_Parser_Callback() = default;
+
+  void start_document() override { start_executed = true; }
+
+  void end_document() override {
+    end_executed = true;
+    REQUIRE(nodes.empty());
+    REQUIRE(ways.empty());
+    REQUIRE(relations.empty());
+  }
+
+  void process_node(const api06::Node &n, operation op, bool if_unused) override {
+    REQUIRE(!nodes.empty());
+
+    auto const& [n_expected, op_expected, if_unused_expected] = nodes.front();
+
+    REQUIRE(n_expected == n);
+    REQUIRE(op == op_expected);
+    REQUIRE(if_unused == if_unused_expected);
+
+    nodes.pop_front();
+  }
+
+  void process_way(const api06::Way &w, operation op, bool if_unused) override {
+    REQUIRE(!ways.empty());
+
+    auto const& [w_expected, op_expected, if_unused_expected] = ways.front();
+
+    REQUIRE(w_expected == w);
+    REQUIRE(op == op_expected);
+    REQUIRE(if_unused == if_unused_expected);
+
+    ways.pop_front();
+  }
+
+  void process_relation(const api06::Relation &r, operation op, bool if_unused) override {
+    REQUIRE(!relations.empty());
+
+    auto const& [r_expected, op_expected, if_unused_expected] = relations.front();
+
+    REQUIRE(r_expected == r);
+    REQUIRE(op == op_expected);
+    REQUIRE(if_unused == if_unused_expected);
+
+    relations.pop_front();
+  }
+
+  bool start_executed{false};
+  bool end_executed{false};
+
+  using node_tuple = std::tuple<api06::Node, operation, bool>;
+  using way_tuple = std::tuple<api06::Way, operation, bool>;
+  using relation_tuple = std::tuple<api06::Relation, operation, bool>;
+
+  std::list< node_tuple > nodes;
+  std::list< way_tuple> ways;
+  std::list< relation_tuple > relations;
+};
+
+class global_settings_test_class : public global_settings_default {
+
+public:
+
+  std::optional<uint32_t> get_relation_max_members() const override {
+     return m_relation_max_members;
+  }
+
+  std::optional<uint32_t> get_element_max_tags() const override {
+     return m_element_max_tags;
+  }
+
+  std::optional<uint32_t> m_relation_max_members{};
+  std::optional<uint32_t> m_element_max_tags{};
+
+};
+
+std::string repeat(const std::string &input, size_t num) {
+  std::ostringstream os;
+  std::fill_n(std::ostream_iterator<std::string>(os), num, input);
+  return os.str();
+}
+
+void process_testmsg(const std::string &payload, Test_Parser_Callback& cb) {
+
+  api06::OSMChangeJSONParser parser(cb);
+  parser.process_message(payload);
+
+  REQUIRE(cb.start_executed);
+  REQUIRE(cb.end_executed);
+}
+
+void process_testmsg(const std::string &payload) {
+
+  Test_Parser_Callback cb{};
+  process_testmsg(payload, cb);
+}
+
+
+// OSMCHANGE STRUCTURE TESTS
+
+TEST_CASE("Invalid JSON", "[osmchange][json]") {
+  auto i = GENERATE(R"({})", R"(bla)");
+  REQUIRE_THROWS_AS(process_testmsg(i), http::bad_request);
+}
+/*
+
+TEST_CASE("XML without any changes", "[osmchange][json]") {
+  REQUIRE_NOTHROW(process_testmsg(R"(<osmChange/>)"));
+}
+
+TEST_CASE("Invalid XML: osmchange end only", "[osmchange][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(R"(</osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Misspelled osmchange xml", "[osmchange][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(R"(<osmChange2/>)"), http::bad_request);
+}
+
+TEST_CASE("osmchange: Unknown action", "[osmchange][json]") {
+  REQUIRE_THROWS_MATCHES(process_testmsg(R"(<osmChange><dummy/></osmChange>)"), http::bad_request,
+    Catch::Message("Unknown action dummy, choices are create, modify, delete at line 1, column 18"));
+}
+
+TEST_CASE("osmchange: Empty create action", "[osmchange][json]") {
+  REQUIRE_NOTHROW(process_testmsg(R"(<osmChange><create/></osmChange>)"));
+}
+
+TEST_CASE("osmchange: Empty modify action", "[osmchange][json]") {
+  REQUIRE_NOTHROW(process_testmsg(R"(<osmChange><modify/></osmChange>)"));
+}
+
+TEST_CASE("osmchange: Empty delete action", "[osmchange][json]") {
+  REQUIRE_NOTHROW(process_testmsg(R"(<osmChange><delete/></osmChange>)"));
+}
+
+TEST_CASE("osmchange: create invalid object", "[osmchange][json]") {
+  REQUIRE_THROWS_MATCHES(process_testmsg(R"(<osmChange><create><bla/></create></osmChange>)"), http::bad_request,
+    Catch::Message("Unknown element bla, expecting node, way or relation at line 1, column 24"));
+}
+
+*/
+
+// NODE TESTS
+
+TEST_CASE("Create empty node without details", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(R"({"osmChange": [{ "type": "node", "action": "create"}]})"), http::bad_request);
+}
+
+TEST_CASE("Create node, details except changeset info missing", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(R"({"osmChange": [{ "type": "node", "action": "create", changeset: 1}]})"), http::bad_request);
+}
+
+TEST_CASE("Create node, lat lon missing", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(R"({"osmChange": [{ "type": "node", "action": "create", changeset: 12, id: -1}]})"), http::bad_request);
+}
+
+/*
+TEST_CASE("Create node, lat missing", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(R"(<osmChange><create><node changeset="858" id="-1" lon="2"/></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create node, lon missing", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(R"(<osmChange><create><node changeset="858" id="-1" lat="2"/></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create node, lat outside range", "[osmchange][node][json]") {
+  auto i = GENERATE(R"(90.01)", R"(-90.01)");
+  REQUIRE_THROWS_AS(process_testmsg(fmt::format(R"(<osmChange><create><node changeset="858" id="-1" lat="{}" lon="2"/></create></osmChange>)", i)), http::bad_request);
+}
+
+TEST_CASE("Create node, lon outside range", "[osmchange][node][json]") {
+  auto i = GENERATE(R"(180.01)", R"(-180.01)");
+  REQUIRE_THROWS_AS(process_testmsg(fmt::format(R"(<osmChange><create><node changeset="858" id="-1" lat="90.00" lon="{}"/></create></osmChange>)", i)), http::bad_request);
+}
+
+TEST_CASE("Create node, lat float overflow", "[osmchange][node][json]") {
+  auto i = GENERATE(R"(9999999999999999999999999999999999999999999999.01)", R"(-9999999999999999999999999999999999999999999999.01)");
+  REQUIRE_THROWS_AS(process_testmsg(fmt::format(R"(<osmChange><create><node changeset="858" id="-1" lat="{}" lon="2"/></create></osmChange>)", i)), http::bad_request);
+}
+
+TEST_CASE("Create node, lon float overflow", "[osmchange][node][json]") {
+  auto i = GENERATE(R"(9999999999999999999999999999999999999999999999.01)", R"(-9999999999999999999999999999999999999999999999.01)");
+  REQUIRE_THROWS_AS(process_testmsg(fmt::format(R"(<osmChange><create><node changeset="858" id="-1" lat="90.00" lon="{}"/></create></osmChange>)", i)), http::bad_request);
+}
+
+TEST_CASE("Create node, lat non-finite float", "[osmchange][node][json]") {
+  auto i = GENERATE(R"(nan)", R"(-nan)", R"(Inf), R"(-Inf)");
+  REQUIRE_THROWS_AS(process_testmsg(fmt::format(R"(<osmChange><create><node changeset="858" id="-1" lat="{}" lon="2"/></create></osmChange>)", i)), http::bad_request);
+}
+
+TEST_CASE("Create node, lon non-finite float", "[osmchange][node][json]") {
+  auto i = GENERATE(R"(nan)", R"(-nan)", R"(Inf), R"(-Inf)");
+  REQUIRE_THROWS_AS(process_testmsg(fmt::format(R"(<osmChange><create><node changeset="858" id="-1" lat="90.00" lon="{}"/></create></osmChange>)", i)), http::bad_request);
+}
+
+TEST_CASE("Create node, changeset missing", "[osmchange][node][json]") {
+  REQUIRE_THROWS_MATCHES(process_testmsg(R"(<osmChange><create><node id="-1" lat="-90.00" lon="-180.00"/></create></osmChange>)"), http::bad_request,
+    Catch::Message("Changeset id is missing for Node -1 at line 1, column 60"));
+}
+
+TEST_CASE("Create node, redefined lat attribute", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(R"(<osmChange><create><node changeset="858" id="-1" lat="-90.00" lon="-180.00" lat="20"/></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create valid node", "[osmchange][node][json]") {
+  auto i = GENERATE(R"(<osmChange><create><node changeset="858" id="-1" lat="90.00" lon="180.00"/></create></osmChange>)",
+                    R"(<osmChange><create><node changeset="858" id="-1" lat="-90.00" lon="-180.00"/></create></osmChange>)");
+  REQUIRE_NOTHROW(process_testmsg(i));
+}
+
+TEST_CASE("Modify node, missing version", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(R"(<osmChange><modify><node changeset="858" id="123" lat="90.00" lon="180.00"/></modify></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Modify node, invalid version", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(R"(<osmChange><modify><node changeset="858" version="0" id="123"/></modify></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Delete node", "[osmchange][node][json]") {
+  REQUIRE_NOTHROW(process_testmsg(R"(<osmChange><delete><node changeset="858" version="1" id="123"/></delete></osmChange>)"));
+}
+
+TEST_CASE("Delete node, if-unused", "[osmchange][node][json]") {
+  REQUIRE_NOTHROW(process_testmsg(R"(<osmChange><delete if-unused="true"><node changeset="858" version="1" id="123"/></delete></osmChange>)"));
+}
+
+TEST_CASE("Delete node, missing version", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(R"(<osmChange><delete><node changeset="858" id="123"/></delete></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Delete node, invalid version", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(R"(<osmChange><delete><node changeset="858" version="0" id="123"/></modify></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Delete node, missing id", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(R"(<osmChange><delete><node changeset="858" version="1"/></modify></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create node, extra xml nested inside tag", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><node changeset="858" id="-1" lat="-90.00" lon="-180.00">
+        <tag k="1" v="2"><blubb/></tag></node></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create node, empty tag key", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><node changeset="858" id="-1" lat="-1" lon="2">
+        <tag k="" v="value"/></node></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create node, empty tag value", "[osmchange][node][json]") {
+  REQUIRE_NOTHROW(process_testmsg(
+    R"(<osmChange><create><node changeset="858" id="-1" lat="-1" lon="2">
+        <tag k="key" v=""/></node></create></osmChange>)"));
+}
+
+TEST_CASE("Create node, duplicate key dup1", "[osmchange][node][json]") {
+  REQUIRE_THROWS_MATCHES(process_testmsg(
+    R"(<osmChange><create><node changeset="858" id="-1" lat="-1" lon="2">
+                       <tag k="key1" v="value1"/>
+                       <tag k="dup1" v="value2"/>
+                       <tag k="dup1" v="value3"/>
+                       <tag k="key3" v="value4"/>
+                       </node></create></osmChange>)"),
+    http::bad_request, Catch::Message("Node -1 has duplicate tags with key dup1 at line 4, column 48"));
+}
+
+TEST_CASE("Create node, tag without value", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><node changeset="858" id="-1" lat="-1" lon="2">
+                       <tag k="key"/></node></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create node, tag without key", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><node changeset="858" id="-1" lat="-1" lon="2">
+                       <tag v="value"/></node></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create node, tag value with <= 255 unicode characters", "[osmchange][node][json]") {
+  for (int i = 0; i <= 255; i++) {
+    auto v = repeat("😎", i);
+    REQUIRE_NOTHROW(process_testmsg(
+      fmt::format(R"(<osmChange><create><node changeset="858" id="-1" lat="-1" lon="2">
+                            <tag k="key" v="{}"/></node></create></osmChange>)", v)));
+  }
+}
+
+TEST_CASE("Create node, tag value with > 255 unicode characters", "[osmchange][node][json]") {
+  REQUIRE_THROWS_MATCHES(process_testmsg(
+    fmt::format(R"(<osmChange><create><node changeset="858" id="-1" lat="-1" lon="2">
+                           <tag k="key" v="{}"/></node></create></osmChange>)", repeat("😎", 256))),
+    http::bad_request, Catch::Message("Value has more than 255 unicode characters in Node -1 at line 2, column 301"));
+}
+
+TEST_CASE("Create node, tag key with <= 255 unicode characters", "[osmchange][node][json]") {
+  for (int i = 1; i <= 255; i++) {
+    auto v = repeat("😎", i);
+    REQUIRE_NOTHROW(process_testmsg(
+      fmt::format(R"(<osmChange><create><node changeset="858" id="-1" lat="-1" lon="2">
+                           <tag k="{}" v="value"/></node></create></osmChange>)", v)));
+  }
+}
+
+TEST_CASE("Create node, tag key with > 255 unicode characters", "[osmchange][node][json]") {
+  REQUIRE_THROWS_MATCHES(process_testmsg(
+    fmt::format(R"(<osmChange><create><node changeset="858" id="-1" lat="-1" lon="2">
+                           <tag k="{}" v="value"/></node></create></osmChange>)", repeat("😎", 256))),
+    http::bad_request, Catch::Message("Key has more than 255 unicode characters in Node -1 at line 2, column 303"));
+}
+
+
+// NODE: INVALID ARGUMENTS, OUT OF RANGE VALUES
+
+TEST_CASE("Modify node, invalid version number", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><modify><node changeset="858" version="a" id="123"/></modify></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Modify node, version too large", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><modify><node changeset="858" version="999999999999999999999999999999999999" id="123"/></modify></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Modify node, version negative", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><modify><node changeset="858" version="-1" id="123"/></modify></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create node, invalid changeset number", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><node changeset="a"/></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create node, changeset number too large", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><node changeset="999999999999999999999999999999999999" id="-1" lat="1" lon="0"/></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create node, changeset number zero", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><node changeset="0" id="-1" lat="1" lon="0"/></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create node, changeset number negative", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><node changeset="-1" id="-1" lat="1" lon="0"/></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create node, longitude not numeric", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><node changeset="858" id="-1" lat="90.00" lon="a"/></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create node, latitude not numeric", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><node changeset="858" id="-1" lat="a" lon="0"/></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create node, invalid id", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><node id="a" changeset="1"/></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create node, id too large", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><node changeset="1" id="999999999999999999999999999999999999" lat="1" lon="0"/></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create node, id zero", "[osmchange][node][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><node changeset="1" id="0" lat="1" lon="0"/></create></osmChange>)"), http::bad_request);
+}
+
+
+
+// WAY TESTS
+
+TEST_CASE("Create way, no details", "[osmchange][way][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><way/></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create way, only changeset", "[osmchange][way][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><way changeset="123"/></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create way, missing changeset", "[osmchange][way][json]") {
+  REQUIRE_THROWS_MATCHES(process_testmsg(
+    R"(<osmChange><create><way id="-1"/></create></osmChange>)"),
+    http::bad_request, Catch::Message("Changeset id is missing for Way -1 at line 1, column 32"));
+}
+
+TEST_CASE("Create way, missing node ref", "[osmchange][way][json]") {
+  REQUIRE_THROWS_MATCHES(process_testmsg(
+    R"(<osmChange><create><way changeset="858" id="-1"/></create></osmChange>)"),
+    http::precondition_failed, Catch::Message("Precondition failed: Way -1 must have at least one node"));
+}
+
+TEST_CASE("Create way, node refs < max way nodes", "[osmchange][way][json]") {
+  std::string node_refs{};
+  for (uint32_t i = 1; i <= global_settings::get_way_max_nodes(); i++) {
+    node_refs += fmt::format(R"(<nd ref="-{}"/>)",  i);
+    REQUIRE_NOTHROW(process_testmsg(
+      fmt::format(R"(<osmChange><create><way changeset="858" id="-1">{}</way></create></osmChange>)", node_refs)));
+  }
+}
+
+TEST_CASE("Create way, node refs >= max way nodes", "[osmchange][way][json]") {
+  std::string node_refs{};
+  for (uint32_t i = 1; i <= global_settings::get_way_max_nodes(); i++)
+    node_refs += fmt::format(R"(<nd ref="-{}"/>)", i);
+  for (uint32_t j = global_settings::get_way_max_nodes()+1; j < global_settings::get_way_max_nodes() + 10; ++j) {
+    node_refs += fmt::format(R"(<nd ref="-{}"/>)", j);
+    REQUIRE_THROWS_MATCHES(process_testmsg(
+      fmt::format(R"(<osmChange><create><way changeset="858" id="-1">{}</way></create></osmChange>)", node_refs)),
+      http::bad_request, Catch::Message(fmt::format("You tried to add {} nodes to way -1, however only {} are allowed", j, global_settings::get_way_max_nodes())));
+  }
+}
+
+TEST_CASE("Create way, with tags", "[osmchange][way][json]") {
+  REQUIRE_NOTHROW(process_testmsg(
+    R"(<osmChange><create><way changeset="858" id="-1"><nd ref="-1"/><tag k="key" v="value"/></way></create></osmChange>)"));
+}
+
+TEST_CASE("Create way, node ref not numeric", "[osmchange][way][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><way changeset="858" id="-1"><nd ref="a"/><tag k="key" v="value"/></way></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create way, node ref too large", "[osmchange][way][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><way changeset="858" id="-1"><nd ref="999999999999999999999"/><tag k="key" v="value"/></way></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create way, invalid zero node ref", "[osmchange][way][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><way changeset="858" id="-1"><nd ref="0"/><tag k="key" v="value"/></way></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create way, node ref missing", "[osmchange][way][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><way changeset="858" id="-1"><nd ref="1"/><nd /><tag k="key" v="value"/></way></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Delete way, no version", "[osmchange][way][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><delete><way changeset="858" id="-1"/></delete></osmChange>)"),
+    http::bad_request);
+}
+
+TEST_CASE("Delete way, no id", "[osmchange][way][json]") {
+  REQUIRE_THROWS_MATCHES(process_testmsg(
+    R"(<osmChange><delete><way changeset="858" version="1"/></delete></osmChange>)"),
+    http::bad_request, Catch::Message(fmt::format("Mandatory field id missing in object at line 1, column 52")));
+}
+
+TEST_CASE("Delete way, no changeset", "[osmchange][way][json]") {
+  REQUIRE_THROWS_MATCHES(process_testmsg(
+    R"(<osmChange><delete><way id="-1" version="1"/></delete></osmChange>)"),
+    http::bad_request, Catch::Message(fmt::format("Changeset id is missing for Way -1 at line 1, column 44")));
+}
+
+TEST_CASE("Delete way", "[osmchange][way][json]") {
+  REQUIRE_NOTHROW(process_testmsg(R"(<osmChange><delete><way changeset="858" id="-1" version="1"/></delete></osmChange>)"));
+}
+
+
+// RELATION TESTS
+
+TEST_CASE("Create relation, id missing", "[osmchange][relation][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><relation changeset="972"><member type="node" ref="1" role="stop"/></relation></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create relation, member ref missing", "[osmchange][relation][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><relation changeset="972" id="-1"><member type="node" role="stop"/></relation></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create relation, no member role", "[osmchange][relation][json]") {
+  REQUIRE_NOTHROW(process_testmsg(
+    R"(<osmChange><create><relation changeset="972" id="-1"><member type="node" ref="-1"/></relation></create></osmChange>)"));
+}
+
+TEST_CASE("Create relation, member type missing", "[osmchange][relation][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><relation changeset="972" id="-1"><member role="stop" ref="-1"/></relation></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create relation, invalid member type", "[osmchange][relation][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><relation changeset="972" id="-1"><member type="bla" role="stop" ref="-1"/></relation></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create relation, invalid member ref", "[osmchange][relation][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><relation changeset="972" id="-1"><member type="node" ref="a" role="stop"/></relation></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create relation, invalid member ref zero", "[osmchange][relation][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><relation changeset="972" id="-1"><member type="way" ref="0" role="stop"/></relation></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create relation, member ref too large", "[osmchange][relation][json]") {
+  REQUIRE_THROWS_AS(process_testmsg(
+    R"(<osmChange><create><relation changeset="972" id="-1">
+           <member type="relation" ref="99999999999999999999999999999999" role="stop"/>
+           </relation></create></osmChange>)"), http::bad_request);
+}
+
+TEST_CASE("Create relation, role with <= 255 unicode characters", "[osmchange][relation][json]") {
+  for (int i = 1; i <= 255; i++) {
+    auto v = repeat("😎", i);
+    REQUIRE_NOTHROW(process_testmsg(
+      fmt::format(
+               R"(<osmChange><create><relation changeset="858" id="-1">
+                           <member type="node" role="{}" ref="123"/>
+                  </relation></create></osmChange>)",
+           v)));
+  }
+}
+
+TEST_CASE("Create relation, role with > 255 unicode characters", "[osmchange][relation][json]") {
+  REQUIRE_THROWS_MATCHES(process_testmsg(
+    fmt::format(
+               R"(<osmChange><create><relation changeset="858" id="-1">
+                           <member type="node" role="{}" ref="123"/>
+                  </relation></create></osmChange>)",
+           repeat("😎", 256))),
+    http::bad_request, Catch::Message("Relation Role has more than 255 unicode characters at line 2, column 321"));
+}
+
+TEST_CASE("Delete relation, no version", "[osmchange][relation][json]") {
+  REQUIRE_THROWS_MATCHES(process_testmsg(
+    R"(<osmChange><delete><relation changeset="972" id="-1"/></delete></osmChange>)"),
+    http::bad_request, Catch::Message(fmt::format("Version is required when updating Relation -1 at line 1, column 53")));
+}
+
+TEST_CASE("Delete relation, no id", "[osmchange][relation][json]") {
+  REQUIRE_THROWS_MATCHES(process_testmsg(
+    R"(<osmChange><delete><relation changeset="972" version="1"/></delete></osmChange>)"),
+    http::bad_request, Catch::Message(fmt::format("Mandatory field id missing in object at line 1, column 57")));
+}
+
+TEST_CASE("Delete relation", "[osmchange][relation][json]") {
+  REQUIRE_NOTHROW(process_testmsg(
+    R"(<osmChange><delete><relation changeset="972" id="123456" version="1"/></delete></osmChange>)"));
+}
+
+// INVALID DATA TESTS
+
+TEST_CASE("Invalid data", "[osmchange][json]") {
+  REQUIRE_THROWS_AS(process_testmsg("\x3C\x00\x00\x00\x00\x0A\x01\x00"), http::bad_request);
+}
+
+*/
+
+// LARGE MESSAGE TESTS
+
+TEST_CASE("Very large JSON message", "[osmchange][node][json]") {
+
+  // Test JSON processing with a very large message
+  std::stringstream s;
+
+  s << R"(
+      {
+        "version": "0.6",
+        "generator": "demo",
+        "osmChange": [  
+     )";
+
+  Test_Parser_Callback cb{};
+
+  for (int i = 1; i < 100000; i++) {
+
+    if (i > 1) {
+      s << ",\n";
+    }
+
+    api06::Node node;
+    node.set_id(-i);
+    node.set_changeset(123);
+    node.add_tags({{"some key", "some value"}});
+
+    switch (i % 3) {
+    case 0:
+      node.set_lat(1);
+      node.set_lon(2);
+      node.set_version(0); // operation create forces version 0, regardless of JSON contents
+
+      cb.nodes.emplace_back(node, operation::op_create, false);
+
+      s << fmt::format(R"(
+          {{
+            "type": "node",
+            "action": "{}",
+            "id": {},
+            "lat": 1,
+            "lon": 2,
+            "changeset": 123,
+            "tags": {{
+              "some key": "some value"
+            }}
+          }}
+         )", "create", -i);
+
+      break;
+
+    case 1:
+      node.set_lat(1);
+      node.set_lon(2);
+      node.set_version(1);
+
+      cb.nodes.emplace_back(node, operation::op_modify, false);
+
+      s << fmt::format(R"(
+          {{
+            "type": "node",
+            "action": "{}",
+            "id": {},
+            "lat": 1,
+            "lon": 2,
+            "version": 1,
+            "changeset": 123,
+            "tags": {{
+              "some key": "some value"
+            }}
+          }}
+         )", "modify", -i);
+      break;
+
+    case 2:
+      node.set_version(1);
+      cb.nodes.emplace_back(node, operation::op_delete, false);
+
+      s << fmt::format(R"(
+          {{
+            "type": "node",
+            "action": "{}",
+            "id": {},
+            "version": 1,
+            "changeset": 123,
+            "tags": {{
+              "some key": "some value"
+            }}
+          }}
+         )", "delete", -i);
+
+      break;
+
+    }
+  }
+
+  s << R"(
+        ]
+      }
+    )";
+
+  REQUIRE_NOTHROW(process_testmsg(s.str(), cb));
+
+}
+
+/*
+
+// OBJECT LIMIT TESTS
+
+TEST_CASE("Create node, tags < max tags", "[osmchange][node][json]") {
+  auto test_settings = std::unique_ptr<global_settings_test_class>(new global_settings_test_class());
+  test_settings->m_element_max_tags = 50;
+
+  global_settings::set_configuration(std::move(test_settings));
+  REQUIRE(global_settings::get_element_max_tags());
+
+  std::string tags{};
+  for (uint32_t i = 1; i <= global_settings::get_element_max_tags(); i++) {
+    tags += fmt::format("<tag k='amenity_{}' v='cafe' />",  i);
+    REQUIRE_NOTHROW(process_testmsg(
+      fmt::format(R"(<osmChange><create><node changeset="858" id="-1" lat="-1" lon="2">{}</node></create></osmChange>)", tags)));
+  }
+}
+
+TEST_CASE("Create node, tags >= max tags", "[osmchange][node][json]") {
+  auto test_settings = std::unique_ptr<global_settings_test_class>(new global_settings_test_class());
+  test_settings->m_element_max_tags = 50;
+
+  global_settings::set_configuration(std::move(test_settings));
+  REQUIRE(global_settings::get_element_max_tags());
+
+  std::string tags{};
+  for (uint32_t i = 1; i <= *global_settings::get_element_max_tags(); i++)
+    tags += fmt::format("<tag k='amenity_{}' v='cafe' />",  i);
+  for (uint32_t j = *global_settings::get_element_max_tags()+1; j < *global_settings::get_element_max_tags() + 10; ++j) {
+    tags += fmt::format("<tag k='amenity_{}' v='cafe' />",  j);
+    REQUIRE_THROWS_AS(process_testmsg(
+      fmt::format(R"(<osmChange><create><node changeset="858" id="-1" lat="-1" lon="2">{}</node></create></osmChange>)", tags)),
+      http::bad_request);
+  }
+}
+
+TEST_CASE("Create relation, members < max members", "[osmchange][relation][json]") {
+  auto test_settings = std::unique_ptr<global_settings_test_class>(new global_settings_test_class());
+  test_settings->m_relation_max_members = 32000;
+
+  global_settings::set_configuration(std::move(test_settings));
+  REQUIRE(global_settings::get_relation_max_members());
+
+  std::string members = repeat(R"(<member type="node" role="demo" ref="123"/>)", *global_settings::get_relation_max_members());
+  REQUIRE_NOTHROW(process_testmsg(
+    fmt::format(R"(<osmChange><create><relation changeset="858" id="-1">{}"</relation></create></osmChange>)", members)));
+}
+
+TEST_CASE("Create relation, members >= max members", "[osmchange][relation][json]") {
+  auto test_settings = std::unique_ptr<global_settings_test_class>(new global_settings_test_class());
+  test_settings->m_relation_max_members = 32000;
+
+  global_settings::set_configuration(std::move(test_settings));
+  REQUIRE(global_settings::get_relation_max_members());
+
+  std::string members = repeat(R"(<member type="node" role="demo" ref="123"/>)", *global_settings::get_relation_max_members());
+  for (uint32_t j = *global_settings::get_relation_max_members()+1; j < *global_settings::get_relation_max_members() + 3; ++j) {
+    members += R"(<member type="node" role="demo" ref="123"/>)";
+    REQUIRE_THROWS_AS(process_testmsg(
+      fmt::format(R"(<osmChange><create><relation changeset="858" id="-1">{}"</relation></create></osmChange>)", members)),
+      http::bad_request);
+  }
+}
+*/
+
+TEST_CASE("Create node", "[osmchange][node][json]") {
+
+  Test_Parser_Callback cb{};
+  api06::Node node;
+  node.set_id(-1);
+  node.set_lat(42.7957187);
+  node.set_lon(13.5690032);
+  node.set_changeset(124176968);
+  node.set_version(0); // operation create forces version 0, regardless of JSON contents
+  node.add_tags({{"man_made", "mast"},{"name", "Monte Piselli - San Giacomo"}});
+
+  cb.nodes.emplace_back(node, operation::op_create, false);
+
+  REQUIRE_NOTHROW(process_testmsg(
+    R"(
+      {
+        "version": "0.6",
+        "generator": "demo",
+        "osmChange": [
+          {
+            "type": "node",
+            "action": "create",
+            "id": -1,
+            "lat": 42.7957187,
+            "lon": 13.5690032,
+            "changeset": 124176968,
+            "tags": {
+              "man_made": "mast",
+              "name": "Monte Piselli - San Giacomo"
+            }
+          }
+        ]
+      }
+    )", cb));
+}
+
+TEST_CASE("Create way", "[osmchange][way][json]") {
+
+  Test_Parser_Callback cb{};
+  api06::Way way;
+  way.set_id(-1);
+  way.set_changeset(124176968);
+  way.set_version(0); // operation create forces version 0, regardless of JSON contents
+  way.add_way_nodes({1,2,3,4});
+  way.add_tags({{"highway", "residential"},{"name", "Via Monte"}});
+
+  cb.ways.emplace_back(way, operation::op_create, false);
+
+  REQUIRE_NOTHROW(process_testmsg(
+    R"(
+      {
+        "version": "0.6",
+        "generator": "demo",
+        "osmChange": [
+          {
+            "type": "way",
+            "action": "create",
+            "id": -1,
+            "changeset": 124176968,
+            "nodes": [1,2,3,4],
+            "tags": {
+              "highway": "residential",
+              "name": "Via Monte"
+            }
+          }
+        ]
+      }
+    )", cb));
+}
+
+TEST_CASE("Create relation", "[osmchange][relation][json]") {
+
+  Test_Parser_Callback cb{};
+  api06::Relation rel;
+  rel.set_id(-1);
+  rel.set_changeset(124176968);
+  rel.set_version(0); // operation create forces version 0, regardless of JSON contents
+  rel.add_tags({{"route", "bus"}, {"ref", "23"}});  // last ref tag wins
+  rel.add_members({{"Node", -1, "stop"}, {"Way", -2, ""}, {"Relation", -3, "parent"}});
+
+  cb.relations.emplace_back(rel, operation::op_create, false);
+
+  REQUIRE_NOTHROW(process_testmsg(
+    R"(
+      {
+        "version": "0.6",
+        "generator": "demo",
+        "osmChange": [
+          {
+            "type": "relation",
+            "action": "create",
+            "id": -1,
+            "changeset": 124176968,
+            "members": [
+                          {"type": "Node", "ref": -1, "role": "stop"},
+                          {"type": "Way", "ref": -2},
+                          {"type": "Relation", "ref": -3, "role": "parent"}
+                       ],
+            "tags": {
+              "ref": "123",
+              "route": "bus",
+              "ref": "23"
+            }
+          }
+        ]
+      }
+    )", cb));
+}