diff --git a/bin/utils/test_file_list.yaml b/bin/utils/test_file_list.yaml
index 86bd12d5d1bb..3ce25badeb3a 100644
--- a/bin/utils/test_file_list.yaml
+++ b/bin/utils/test_file_list.yaml
@@ -10,7 +10,7 @@
- filename: "samples/client/petstore/java/okhttp-gson/src/test/java/org/openapitools/client/ClientTest.java"
sha256: 325fdd5d7e2c97790c0fb44f712ab7b2ba022d7e1a5b0056f47b07f342682b6d
- filename: "samples/client/petstore/java/okhttp-gson/src/test/java/org/openapitools/client/JSONTest.java"
- sha256: ddd53d2f640aee7712299f1540779cdb508f0b9c1e96082191686e218eadb04c
+ sha256: 47293b138c51990bc80299837ed85c00463ccde9c4986fbe51d57246cf5eb0b7
- filename: "samples/client/petstore/java/okhttp-gson/src/test/java/org/openapitools/client/api/PetApiTest.java"
sha256: 8b1b8f2a2ad00ccb090873a94a5f73e328b98317d2ec715f53bd7a1accb2a023
- filename: "samples/client/petstore/java/okhttp-gson/src/test/java/org/openapitools/client/model/PetTest.java"
diff --git a/modules/openapi-generator/pom.xml b/modules/openapi-generator/pom.xml
index e3de9d3ee18d..27c2e7fe5fc8 100644
--- a/modules/openapi-generator/pom.xml
+++ b/modules/openapi-generator/pom.xml
@@ -303,6 +303,31 @@
2.10.1
test
+
+
+ io.gsonfire
+ gson-fire
+ 1.9.0
+ test
+
+
+ com.squareup.okio
+ okio
+ 3.2.0
+ test
+
org.eclipse
yasson
diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaOkHttpGsonNullAdditionalPropertiesTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaOkHttpGsonNullAdditionalPropertiesTest.java
new file mode 100644
index 000000000000..7c2d9a5ceef4
--- /dev/null
+++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/java/JavaOkHttpGsonNullAdditionalPropertiesTest.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2026 OpenAPI-Generator Contributors (https://openapi-generator.tech)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.openapitools.codegen.java;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import io.gsonfire.GsonFireBuilder;
+import okio.ByteString;
+import org.openapitools.codegen.DefaultGenerator;
+import org.openapitools.codegen.config.CodegenConfigurator;
+import org.testng.annotations.Test;
+
+import javax.tools.ToolProvider;
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Map;
+
+import static org.openapitools.codegen.TestUtils.newTempFolder;
+import static org.testng.Assert.*;
+
+public class JavaOkHttpGsonNullAdditionalPropertiesTest {
+
+ @Test
+ public void testNullAdditionalPropertiesRoundTrip() throws Exception {
+ Path output = newTempFolder();
+ CodegenConfigurator config = new CodegenConfigurator().setGeneratorName("java").setLibrary("okhttp-gson")
+ .setInputSpec("src/test/resources/3_0/java/okhttp-gson-null-additional-properties.yaml")
+ .setOutputDir(output.toString())
+ .addGlobalProperty("models", "").addGlobalProperty("supportingFiles", "")
+ .addGlobalProperty("modelDocs", "false").addGlobalProperty("modelTests", "false");
+ new DefaultGenerator().opts(config.toClientOptInput()).generate();
+
+ // Compile the generated client sources (the JSON factory and the model) against the real
+ // Gson runtime, mirroring the sample-side JSONTest setup: the model's CustomTypeAdapterFactory
+ // is registered by the generated JSON class, and JSON.java plus the generated model need the
+ // client's runtime dependencies (gson-fire, okio, jsr305 @Nullable) on the compile classpath.
+ Path generatedSource = output.resolve("src/main/java");
+ Path jsonSource = generatedSource.resolve("org/openapitools/client/JSON.java");
+ Path modelSource = generatedSource.resolve("org/openapitools/client/model/AdditionalPropertiesModel.java");
+ String classpath = Paths.get(Gson.class.getProtectionDomain().getCodeSource().getLocation().toURI())
+ + File.pathSeparator
+ + Paths.get(GsonFireBuilder.class.getProtectionDomain().getCodeSource().getLocation().toURI())
+ + File.pathSeparator
+ + Paths.get(ByteString.class.getProtectionDomain().getCodeSource().getLocation().toURI())
+ + File.pathSeparator
+ + Paths.get(javax.annotation.Nullable.class.getProtectionDomain().getCodeSource().getLocation().toURI())
+ + File.pathSeparator
+ + Paths.get(kotlin.jvm.internal.Intrinsics.class.getProtectionDomain().getCodeSource().getLocation().toURI());
+ ByteArrayOutputStream diagnostics = new ByteArrayOutputStream();
+ assertNotNull(ToolProvider.getSystemJavaCompiler());
+ assertEquals(ToolProvider.getSystemJavaCompiler().run(null, diagnostics, diagnostics,
+ "--release", "8", "-classpath", classpath, "-d", output.toString(),
+ jsonSource.toString(), modelSource.toString()),
+ 0, diagnostics.toString(StandardCharsets.UTF_8));
+
+ try (URLClassLoader loader = new URLClassLoader(new URL[]{output.toUri().toURL()}, getClass().getClassLoader())) {
+ Class> jsonType = loader.loadClass("org.openapitools.client.JSON");
+ Class> modelType = loader.loadClass("org.openapitools.client.model.AdditionalPropertiesModel");
+ // the client's configured Gson (JSON.getGson), not a hand-built one: its static
+ // initializer registers the generated CustomTypeAdapterFactory for the model
+ Gson gson = (Gson) jsonType.getMethod("getGson").invoke(null);
+ // same pattern as the sample JSONTest: serializeNulls so named nulls are written
+ Gson gsonWithNulls = gson.newBuilder().serializeNulls().create();
+
+ // build the model with a null undeclared-property value
+ Object model = modelType.getConstructor().newInstance();
+ modelType.getMethod("setName", String.class).invoke(model, "x");
+ modelType.getMethod("putAdditionalProperty", String.class, Object.class).invoke(model, "unknownField", null);
+
+ // write: a null undeclared-property value used to throw
+ // IllegalStateException("Not a JSON Object: null") in the CustomTypeAdapterFactory;
+ // the ApiClient's writer omits named nulls, but must not throw
+ assertEquals(gson.toJson(model, modelType), "{\"name\":\"x\"}");
+
+ // serialize through the client's Gson and inspect the tree
+ String serialized = gsonWithNulls.toJson(model, modelType);
+ assertEquals(serialized, "{\"name\":\"x\",\"unknownField\":null}");
+ JsonObject obj = gsonWithNulls.toJsonTree(model, modelType).getAsJsonObject();
+ assertTrue(obj.get("unknownField").isJsonNull(), "A null additional property must be written as JSON null");
+ assertEquals(obj.get("name").getAsString(), "x");
+ assertNull(obj.get("additionalProperties"), "The raw additionalProperties field must be removed from the output");
+
+ // full round trip: deserialize the resulting JSON (not a hand-written string)
+ Object deserialized = gsonWithNulls.fromJson(serialized, modelType);
+ assertEquals(modelType.getMethod("getName").invoke(deserialized), "x");
+ Map, ?> props = (Map, ?>) modelType.getMethod("getAdditionalProperties").invoke(deserialized);
+ assertTrue(props.containsKey("unknownField"), "A JSON null undeclared property must be preserved by the round trip");
+ assertNull(props.get("unknownField"));
+
+ // serialize again: the round trip must be byte-stable
+ assertEquals(gsonWithNulls.toJson(deserialized, modelType), serialized, "Re-serialization must be byte-stable");
+ assertEquals(gson.toJson(deserialized, modelType), "{\"name\":\"x\"}");
+
+ // keep the existing behavior for non-null additional properties
+ modelType.getMethod("putAdditionalProperty", String.class, Object.class).invoke(model, "extra", "e");
+ modelType.getMethod("putAdditionalProperty", String.class, Object.class).invoke(model, "list", Arrays.asList("a", "b"));
+ modelType.getMethod("putAdditionalProperty", String.class, Object.class).invoke(model, "nested", Collections.singletonMap("k", "v"));
+ JsonElement tree = gsonWithNulls.toJsonTree(model, modelType);
+ obj = tree.getAsJsonObject();
+ assertEquals(obj.get("extra").getAsString(), "e");
+ assertTrue(obj.get("list").isJsonArray());
+ assertTrue(obj.get("nested").isJsonObject());
+ }
+ }
+}
diff --git a/modules/openapi-generator/src/test/resources/3_0/java/okhttp-gson-null-additional-properties.yaml b/modules/openapi-generator/src/test/resources/3_0/java/okhttp-gson-null-additional-properties.yaml
new file mode 100644
index 000000000000..581cbf5fce6e
--- /dev/null
+++ b/modules/openapi-generator/src/test/resources/3_0/java/okhttp-gson-null-additional-properties.yaml
@@ -0,0 +1,18 @@
+openapi: 3.0.3
+info: {title: t, version: '1'}
+paths:
+ /m:
+ get:
+ responses:
+ '200':
+ description: ok
+ content:
+ application/json:
+ schema: {$ref: '#/components/schemas/AdditionalPropertiesModel'}
+components:
+ schemas:
+ AdditionalPropertiesModel:
+ type: object
+ properties:
+ name: {type: string}
+ additionalProperties: true
diff --git a/samples/client/petstore/java/okhttp-gson/src/test/java/org/openapitools/client/JSONTest.java b/samples/client/petstore/java/okhttp-gson/src/test/java/org/openapitools/client/JSONTest.java
index 86c22dc25e15..4da140e713ec 100644
--- a/samples/client/petstore/java/okhttp-gson/src/test/java/org/openapitools/client/JSONTest.java
+++ b/samples/client/petstore/java/okhttp-gson/src/test/java/org/openapitools/client/JSONTest.java
@@ -765,6 +765,39 @@ public void testAdditionalArrayProperties() throws IOException {
assertEquals(z.toJson(), "{\"className\":\"zebra\",\"object_array\":[{\"id\":34.0,\"name\":\"just a tag\"}],\"empty_array\":[],\"array\":[\"1\",\"2\",\"3\"],\"new_array\":[\"1\",\"2\",\"3\"],\"new_empty_array\":[],\"new_object_array\":[{\"id\":34,\"name\":\"just a tag\"}]}");
}
+ /**
+ * Test additional properties with a null value.
+ *
+ * Regression test for https://github.com/OpenAPITools/openapi-generator/issues/24393:
+ * the CustomTypeAdapterFactory used to throw IllegalStateException("Not a JSON Object: null")
+ * when serializing a model whose undeclared-property map contains a null value.
+ */
+ @Test
+ public void testAdditionalPropertiesNullValue() throws Exception {
+ // serialize: a null undeclared-property value no longer throws and is written as JSON null
+ AdditionalPropertiesClass model = new AdditionalPropertiesClass();
+ model.putAdditionalProperty("unknownField", null);
+ // Gson drops named nulls unless serializeNulls is enabled
+ Gson gsonWithNulls = json.getGson().newBuilder().serializeNulls().create();
+ JsonObject serialized = gsonWithNulls.toJsonTree(model).getAsJsonObject();
+ assertTrue(serialized.get("unknownField").isJsonNull());
+ // the ApiClient's Gson does not enable serializeNulls, so the named null is omitted from the text output
+ assertFalse(model.toJson().contains("unknownField"));
+
+ // deserialize: JSON null undeclared properties are stored as null-valued entries
+ AdditionalPropertiesClass deserialized = AdditionalPropertiesClass.fromJson("{\"map_property\":{\"key\":\"value\"},\"unknownField\":null}");
+ assertEquals(deserialized.getMapProperty(), Collections.singletonMap("key", "value"));
+ assertTrue(deserialized.getAdditionalProperties().containsKey("unknownField"));
+ assertNull(deserialized.getAdditionalProperties().get("unknownField"));
+
+ // serialize -> deserialize -> serialize round trip preserves the null entry
+ JsonObject roundTripped = gsonWithNulls.toJsonTree(deserialized).getAsJsonObject();
+ assertEquals(roundTripped.get("map_property").getAsJsonObject().get("key").getAsString(), "value");
+ assertTrue(roundTripped.get("unknownField").isJsonNull());
+ // text re-serialization does not throw
+ deserialized.toJson();
+ }
+
/**
* Validate a oneOf, anyOf schema with array sub-schema can be deserialized into the expected class.
*/