fix tests
diff --git a/foreign/cpp/include/iggy.hpp b/foreign/cpp/include/iggy.hpp
index 22906c6..19dc07b 100644
--- a/foreign/cpp/include/iggy.hpp
+++ b/foreign/cpp/include/iggy.hpp
@@ -1073,7 +1073,7 @@
      *         `DEFAULT_PARTITIONS_COUNT` (1).
      * @note Not an option key. Fills the `CreateTopic` command's fixed field;
      *       it is consumed to compute assignments and is not stored as a topic
-     *       option. Must be `1..=1000` when set; server rejects `0`.
+     *       option. Must be `0..=1000` when set.
      */
     std::optional<std::uint32_t> PartitionsCount() const noexcept { return partitions_count_; }
     TopicCreateOptions &SetPartitionsCount(std::uint32_t partitions_count) noexcept {
@@ -1100,11 +1100,16 @@
      *         `IggyExpiry::ServerDefault` (alias `never_expire` with sentinel
      *         `u64::MAX` on the wire as `Uint64`).
      * @note Catalog key `message_expiry` (`Uint64` micros, or `String` like
-     *       `"7 days"` via `Raw`). `0` normalizes to `nullopt`. Also updatable.
+     *       `"7 days"` via `Raw`). `ServerDefault()` normalizes to `nullopt`.
+     *       Also updatable.
      */
     std::optional<::iggy::Expiry> MessageExpiry() const noexcept { return message_expiry_; }
     TopicCreateOptions &SetMessageExpiry(::iggy::Expiry message_expiry) {
-        message_expiry_ = std::move(message_expiry);
+        if (message_expiry.ExpiryKind() == "server_default") {
+            message_expiry_.reset();
+        } else {
+            message_expiry_ = std::move(message_expiry);
+        }
         return *this;
     }
 
@@ -1113,12 +1118,17 @@
      * @return Max size when set; `nullopt` uses the server default
      *         `MaxTopicSize::ServerDefault` (`unlimited`, `u64::MAX` on wire).
      * @note Catalog key `max_topic_size` (`Uint64` bytes or `String` like
-     *       `"1 GiB"` via `Raw`). `0` normalizes to `nullopt`. Must be `>=`
-     *       the resolved segment size when both are set. Also updatable.
+     *       `"1 GiB"` via `Raw`). `ServerDefault()` normalizes to `nullopt`.
+     *       Must be `>=` the resolved segment size when both are set. Also
+     *       updatable.
      */
     std::optional<::iggy::MaxTopicSize> MaxTopicSize() const noexcept { return max_topic_size_; }
     TopicCreateOptions &SetMaxTopicSize(::iggy::MaxTopicSize max_topic_size) {
-        max_topic_size_ = std::move(max_topic_size);
+        if (max_topic_size.MaxTopicSizeValue() == "server_default") {
+            max_topic_size_.reset();
+        } else {
+            max_topic_size_ = std::move(max_topic_size);
+        }
         return *this;
     }
 
@@ -1269,11 +1279,15 @@
      * @brief New message retention policy.
      * @return Expiry when set; `nullopt` keeps the current value.
      * @note Catalog key `message_expiry` (`Uint64` micros or `String` like
-     *       `"7 days"` via `Raw`). `0` is treated as `nullopt` on the Rust side.
+     *       `"7 days"` via `Raw`). `ServerDefault()` normalizes to `nullopt`.
      */
     std::optional<::iggy::Expiry> MessageExpiry() const noexcept { return message_expiry_; }
     TopicUpdateOptions &SetMessageExpiry(::iggy::Expiry message_expiry) {
-        message_expiry_ = std::move(message_expiry);
+        if (message_expiry.ExpiryKind() == "server_default") {
+            message_expiry_.reset();
+        } else {
+            message_expiry_ = std::move(message_expiry);
+        }
         return *this;
     }
 
@@ -1281,11 +1295,15 @@
      * @brief New maximum retained topic size.
      * @return Max size when set; `nullopt` keeps the current value.
      * @note Catalog key `max_topic_size` (`Uint64` bytes or `String` like
-     *       `"1 GiB"` via `Raw`). `0` is treated as `nullopt` on the Rust side.
+     *       `"1 GiB"` via `Raw`). `ServerDefault()` normalizes to `nullopt`.
      */
     std::optional<::iggy::MaxTopicSize> MaxTopicSize() const noexcept { return max_topic_size_; }
     TopicUpdateOptions &SetMaxTopicSize(::iggy::MaxTopicSize max_topic_size) {
-        max_topic_size_ = std::move(max_topic_size);
+        if (max_topic_size.MaxTopicSizeValue() == "server_default") {
+            max_topic_size_.reset();
+        } else {
+            max_topic_size_ = std::move(max_topic_size);
+        }
         return *this;
     }
 
diff --git a/foreign/cpp/tests/e2e/topic.cpp b/foreign/cpp/tests/e2e/topic.cpp
index 5b2355e..916b433 100644
--- a/foreign/cpp/tests/e2e/topic.cpp
+++ b/foreign/cpp/tests/e2e/topic.cpp
@@ -124,21 +124,21 @@
     ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), overflow_topic_name,
                                     iggy::TopicCreateOptions().SetPartitionsCount(1001)),
                  std::exception);
-    ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), zero_partitions_topic_name,
-                                    iggy::TopicCreateOptions().SetPartitionsCount(0)),
-                 std::exception);
+    ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), zero_partitions_topic_name,
+                                       iggy::TopicCreateOptions().SetPartitionsCount(0)));
 
     const auto stream_details = client.GetStream(iggy::Identifier::String(stream_name));
-    EXPECT_EQ(stream_details.TopicsCount(), 2u);
+    EXPECT_EQ(stream_details.TopicsCount(), 3u);
 
     std::unordered_map<std::string, std::uint32_t> topic_partitions;
     for (const auto &topic : stream_details.Topics()) {
         topic_partitions[topic.Name()] = topic.PartitionsCount();
     }
 
-    EXPECT_EQ(topic_partitions.size(), 2u);
+    EXPECT_EQ(topic_partitions.size(), 3u);
     EXPECT_EQ(topic_partitions[default_partitions_topic], 1u);
     EXPECT_EQ(topic_partitions[max_partitions_topic_name], 1000u);
+    EXPECT_EQ(topic_partitions[zero_partitions_topic_name], 0u);
 }
 
 TEST_F(E2E_Topic, CreateTopicWithInvalidNamesThrows) {
@@ -648,25 +648,22 @@
 }
 
 TEST_F(E2E_Topic, GetTopicReturnsEmptyPartitionsForZeroPartitionTopic) {
-    RecordProperty("description", "Rejects zero partitions and creates a topic with the default partition count.");
+    RecordProperty("description", "Returns an empty partitions vector for a topic created with zero partitions.");
     const std::string stream_name = GetRandomName();
     const std::string topic_name  = GetRandomName();
-    const std::string zero_topic  = GetRandomName();
 
     auto client = GetLoggedInHighLevelClient();
     ASSERT_NO_THROW(client.CreateStream(stream_name));
     TrackStream(stream_name);
-    ASSERT_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), zero_topic,
-                                    iggy::TopicCreateOptions().SetPartitionsCount(0)),
-                 std::exception);
-    ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name, iggy::TopicCreateOptions()));
+    ASSERT_NO_THROW(client.CreateTopic(iggy::Identifier::String(stream_name), topic_name,
+                                       iggy::TopicCreateOptions().SetPartitionsCount(0)));
 
     ASSERT_NO_THROW({
         const auto topic_details =
             client.GetTopic(iggy::Identifier::String(stream_name), iggy::Identifier::String(topic_name));
         EXPECT_EQ(topic_details.Name(), topic_name);
-        EXPECT_EQ(topic_details.PartitionsCount(), 1u);
-        EXPECT_EQ(topic_details.Partitions().size(), 1u);
+        EXPECT_EQ(topic_details.PartitionsCount(), 0u);
+        EXPECT_TRUE(topic_details.Partitions().empty());
     });
 }
 
diff --git a/foreign/cpp/tests/unit/unit_tests.cpp b/foreign/cpp/tests/unit/unit_tests.cpp
index e8628f4..6f3eb10 100644
--- a/foreign/cpp/tests/unit/unit_tests.cpp
+++ b/foreign/cpp/tests/unit/unit_tests.cpp
@@ -204,6 +204,17 @@
     EXPECT_EQ(options.MaxTopicSize()->MaxTopicSizeValue(), "1024");
 }
 
+TEST(TopicCreateOptionsTest, ServerDefaultSentinelsClearValues) {
+    iggy::TopicCreateOptions options;
+    options.SetMessageExpiry(iggy::Expiry::Duration(15)).SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024));
+    ASSERT_TRUE(options.MessageExpiry().has_value());
+    ASSERT_TRUE(options.MaxTopicSize().has_value());
+
+    options.SetMessageExpiry(iggy::Expiry::ServerDefault()).SetMaxTopicSize(iggy::MaxTopicSize::ServerDefault());
+    EXPECT_FALSE(options.MessageExpiry().has_value());
+    EXPECT_FALSE(options.MaxTopicSize().has_value());
+}
+
 TEST(TopicCreateOptionsTest, RawMapStoresForwardCompatibleKeys) {
     iggy::TopicCreateOptions options;
     options.SetRawEntries({{"custom_key", "custom_value"}});
@@ -235,6 +246,17 @@
     EXPECT_EQ(options.MaxTopicSize()->MaxTopicSizeValue(), "unlimited");
 }
 
+TEST(TopicUpdateOptionsTest, ServerDefaultSentinelsClearValues) {
+    iggy::TopicUpdateOptions options;
+    options.SetMessageExpiry(iggy::Expiry::Duration(15)).SetMaxTopicSize(iggy::MaxTopicSize::FromBytes(1024));
+    ASSERT_TRUE(options.MessageExpiry().has_value());
+    ASSERT_TRUE(options.MaxTopicSize().has_value());
+
+    options.SetMessageExpiry(iggy::Expiry::ServerDefault()).SetMaxTopicSize(iggy::MaxTopicSize::ServerDefault());
+    EXPECT_FALSE(options.MessageExpiry().has_value());
+    EXPECT_FALSE(options.MaxTopicSize().has_value());
+}
+
 TEST(TopicUpdateOptionsTest, RawMapStoresKeys) {
     iggy::TopicUpdateOptions options;
     options.SetRawEntries({{"message_expiry", "7 days"}});