From 36f6466731adc1c88005f89078c33c5b2b1a17af Mon Sep 17 00:00:00 2001 From: ckoegel Date: Thu, 6 Aug 2026 15:11:45 -0400 Subject: [PATCH 1/4] [Java] [okhttp-gson] Add `anOf` Discriminator Support --- .../okhttp-gson/anyof_model.mustache | 24 +++++++++ .../client/model/MammalAnyof.java | 26 +++++++++ .../org/openapitools/client/JSONTest.java | 53 +++++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/anyof_model.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/anyof_model.mustache index 2fc12731cece..7654881074ee 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/anyof_model.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/anyof_model.mustache @@ -107,6 +107,30 @@ public class {{classname}} extends AbstractOpenApiSchema{{#vendorExtensions.x-im Object deserialized = null; JsonElement jsonElement = elementAdapter.read(in); + {{#useOneOfDiscriminatorLookup}} + {{#discriminator}} + JsonObject jsonObject = jsonElement.getAsJsonObject(); + + // use discriminator value for faster anyOf lookup + {{classname}} new{{classname}} = new {{classname}}(); + if (jsonObject.get("{{{propertyBaseName}}}") == null) { + log.log(Level.WARNING, "Failed to lookup discriminator value for {{classname}} as `{{{propertyBaseName}}}` was not found in the payload or the payload is empty."); + } else { + // look up the discriminator value in the field `{{{propertyBaseName}}}` + switch (jsonObject.get("{{{propertyBaseName}}}").getAsString()) { + {{#mappedModels}} + case "{{{mappingName}}}": + deserialized = adapter{{modelName}}.fromJsonTree(jsonObject); + new{{classname}}.setActualInstance(deserialized); + return new{{classname}}; + {{/mappedModels}} + default: + log.log(Level.WARNING, String.format(java.util.Locale.ROOT, "Failed to lookup discriminator value `%s` for {{classname}}. Possible values:{{#mappedModels}} {{{mappingName}}}{{/mappedModels}}", jsonObject.get("{{{propertyBaseName}}}").getAsString())); + } + } + + {{/discriminator}} + {{/useOneOfDiscriminatorLookup}} ArrayList errorMessages = new ArrayList<>(); TypeAdapter actualAdapter = elementAdapter; diff --git a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/MammalAnyof.java b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/MammalAnyof.java index 54c607cac57a..5b89a95468f3 100644 --- a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/MammalAnyof.java +++ b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/MammalAnyof.java @@ -110,6 +110,32 @@ public MammalAnyof read(JsonReader in) throws IOException { Object deserialized = null; JsonElement jsonElement = elementAdapter.read(in); + JsonObject jsonObject = jsonElement.getAsJsonObject(); + + // use discriminator value for faster anyOf lookup + MammalAnyof newMammalAnyof = new MammalAnyof(); + if (jsonObject.get("className") == null) { + log.log(Level.WARNING, "Failed to lookup discriminator value for MammalAnyof as `className` was not found in the payload or the payload is empty."); + } else { + // look up the discriminator value in the field `className` + switch (jsonObject.get("className").getAsString()) { + case "Pig": + deserialized = adapterPig.fromJsonTree(jsonObject); + newMammalAnyof.setActualInstance(deserialized); + return newMammalAnyof; + case "whale": + deserialized = adapterWhale.fromJsonTree(jsonObject); + newMammalAnyof.setActualInstance(deserialized); + return newMammalAnyof; + case "zebra": + deserialized = adapterZebra.fromJsonTree(jsonObject); + newMammalAnyof.setActualInstance(deserialized); + return newMammalAnyof; + default: + log.log(Level.WARNING, String.format(java.util.Locale.ROOT, "Failed to lookup discriminator value `%s` for MammalAnyof. Possible values: Pig whale zebra", jsonObject.get("className").getAsString())); + } + } + ArrayList errorMessages = new ArrayList<>(); TypeAdapter actualAdapter = elementAdapter; 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 5b6b2241ce28..7ad6263dbfca 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 @@ -499,6 +499,59 @@ public void testOneOfSchemaWithDiscriminator() throws Exception { } } + /** + * Validate an anyOf schema can be deserialized into the expected class. + * The anyOf schema has a discriminator. + */ + @Test + public void testAnyOfSchemaWithDiscriminator() throws Exception { + { + // Zebra is listed after Whale in anyOf, and a zebra payload also satisfies Whale + // (className is Whale's only required field), so without the discriminator lookup + // this deserializes as Whale. + String str = "{ \"className\": \"zebra\", \"type\": \"plains\" }"; + + MammalAnyof o = json.getGson().fromJson(str, MammalAnyof.class); + assertTrue(o.getActualInstance() instanceof Zebra); + Zebra inst = (Zebra) o.getActualInstance(); + assertEquals(inst.getClassName(), "zebra"); + assertEquals(inst.toJson(), "{\"type\":\"plains\",\"className\":\"zebra\"}"); + assertEquals(o.toJson(), "{\"type\":\"plains\",\"className\":\"zebra\"}"); + } + { + String str = "{ \"className\": \"whale\", \"hasBaleen\": false, \"hasTeeth\": false }"; + + MammalAnyof o = json.getGson().fromJson(str, MammalAnyof.class); + assertTrue(o.getActualInstance() instanceof Whale); + Whale inst = (Whale) o.getActualInstance(); + assertEquals(inst.getClassName(), "whale"); + assertEquals(inst.getHasBaleen(), false); + assertEquals(inst.getHasTeeth(), false); + assertEquals(inst.toJson(), "{\"hasBaleen\":false,\"hasTeeth\":false,\"className\":\"whale\"}"); + assertEquals(json.getGson().toJson(o), "{\"hasBaleen\":false,\"hasTeeth\":false,\"className\":\"whale\"}"); + assertEquals(o.toJson(), "{\"hasBaleen\":false,\"hasTeeth\":false,\"className\":\"whale\"}"); + } + { + // an unmapped discriminator value falls back to matching the anyOf schemas in order + String str = "{ \"className\": \"dolphin\", \"hasBaleen\": true }"; + + MammalAnyof o = json.getGson().fromJson(str, MammalAnyof.class); + assertTrue(o.getActualInstance() instanceof Whale); + Whale inst = (Whale) o.getActualInstance(); + assertEquals(inst.getClassName(), "dolphin"); + assertEquals(inst.getHasBaleen(), true); + } + { + // Try to deserialize empty object. This should fail 'anyOf' because none will match + // whale, zebra or Pig. + String str = "{ }"; + Exception exception = assertThrows(com.google.gson.JsonSyntaxException.class, () -> { + json.getGson().fromJson(str, MammalAnyof.class); + }); + assertTrue(exception.getMessage().contains("java.io.IOException: Failed deserialization for MammalAnyof")); + } + } + /** * Test JSON validation method */ From f141753cf2689f97b7a6610af1881e6b2ee30d9c Mon Sep 17 00:00:00 2001 From: ckoegel Date: Thu, 6 Aug 2026 15:43:39 -0400 Subject: [PATCH 2/4] guard clause for primitive types --- .../okhttp-gson/anyof_model.mustache | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/anyof_model.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/anyof_model.mustache index 7654881074ee..082ddb01a402 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/anyof_model.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/anyof_model.mustache @@ -109,23 +109,26 @@ public class {{classname}} extends AbstractOpenApiSchema{{#vendorExtensions.x-im {{#useOneOfDiscriminatorLookup}} {{#discriminator}} - JsonObject jsonObject = jsonElement.getAsJsonObject(); + // non-object payloads can still match a non-object anyOf schema below + if (jsonElement.isJsonObject()) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); - // use discriminator value for faster anyOf lookup - {{classname}} new{{classname}} = new {{classname}}(); - if (jsonObject.get("{{{propertyBaseName}}}") == null) { - log.log(Level.WARNING, "Failed to lookup discriminator value for {{classname}} as `{{{propertyBaseName}}}` was not found in the payload or the payload is empty."); - } else { - // look up the discriminator value in the field `{{{propertyBaseName}}}` - switch (jsonObject.get("{{{propertyBaseName}}}").getAsString()) { - {{#mappedModels}} - case "{{{mappingName}}}": - deserialized = adapter{{modelName}}.fromJsonTree(jsonObject); - new{{classname}}.setActualInstance(deserialized); - return new{{classname}}; - {{/mappedModels}} - default: - log.log(Level.WARNING, String.format(java.util.Locale.ROOT, "Failed to lookup discriminator value `%s` for {{classname}}. Possible values:{{#mappedModels}} {{{mappingName}}}{{/mappedModels}}", jsonObject.get("{{{propertyBaseName}}}").getAsString())); + // use discriminator value for faster anyOf lookup + {{classname}} new{{classname}} = new {{classname}}(); + if (jsonObject.get("{{{propertyBaseName}}}") == null) { + log.log(Level.WARNING, "Failed to lookup discriminator value for {{classname}} as `{{{propertyBaseName}}}` was not found in the payload or the payload is empty."); + } else { + // look up the discriminator value in the field `{{{propertyBaseName}}}` + switch (jsonObject.get("{{{propertyBaseName}}}").getAsString()) { + {{#mappedModels}} + case "{{{mappingName}}}": + deserialized = adapter{{modelName}}.fromJsonTree(jsonObject); + new{{classname}}.setActualInstance(deserialized); + return new{{classname}}; + {{/mappedModels}} + default: + log.log(Level.WARNING, String.format(java.util.Locale.ROOT, "Failed to lookup discriminator value `%s` for {{classname}}. Possible values:{{#mappedModels}} {{{mappingName}}}{{/mappedModels}}", jsonObject.get("{{{propertyBaseName}}}").getAsString())); + } } } From b19ca69db3cc08ffec1462b832863d67b1ffbf0a Mon Sep 17 00:00:00 2001 From: ckoegel Date: Thu, 6 Aug 2026 15:44:36 -0400 Subject: [PATCH 3/4] fix cubic json-null/non-string discriminator feedback --- .../Java/libraries/okhttp-gson/anyof_model.mustache | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/anyof_model.mustache b/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/anyof_model.mustache index 082ddb01a402..e1760876459d 100644 --- a/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/anyof_model.mustache +++ b/modules/openapi-generator/src/main/resources/Java/libraries/okhttp-gson/anyof_model.mustache @@ -112,14 +112,15 @@ public class {{classname}} extends AbstractOpenApiSchema{{#vendorExtensions.x-im // non-object payloads can still match a non-object anyOf schema below if (jsonElement.isJsonObject()) { JsonObject jsonObject = jsonElement.getAsJsonObject(); + JsonElement discriminatorElement = jsonObject.get("{{{propertyBaseName}}}"); // use discriminator value for faster anyOf lookup {{classname}} new{{classname}} = new {{classname}}(); - if (jsonObject.get("{{{propertyBaseName}}}") == null) { - log.log(Level.WARNING, "Failed to lookup discriminator value for {{classname}} as `{{{propertyBaseName}}}` was not found in the payload or the payload is empty."); + if (discriminatorElement == null || !discriminatorElement.isJsonPrimitive()) { + log.log(Level.WARNING, "Failed to lookup discriminator value for {{classname}} as `{{{propertyBaseName}}}` is missing or is not a primitive type in the payload."); } else { // look up the discriminator value in the field `{{{propertyBaseName}}}` - switch (jsonObject.get("{{{propertyBaseName}}}").getAsString()) { + switch (discriminatorElement.getAsString()) { {{#mappedModels}} case "{{{mappingName}}}": deserialized = adapter{{modelName}}.fromJsonTree(jsonObject); @@ -127,7 +128,7 @@ public class {{classname}} extends AbstractOpenApiSchema{{#vendorExtensions.x-im return new{{classname}}; {{/mappedModels}} default: - log.log(Level.WARNING, String.format(java.util.Locale.ROOT, "Failed to lookup discriminator value `%s` for {{classname}}. Possible values:{{#mappedModels}} {{{mappingName}}}{{/mappedModels}}", jsonObject.get("{{{propertyBaseName}}}").getAsString())); + log.log(Level.WARNING, String.format(java.util.Locale.ROOT, "Failed to lookup discriminator value `%s` for {{classname}}. Possible values:{{#mappedModels}} {{{mappingName}}}{{/mappedModels}}", discriminatorElement.getAsString())); } } } From ebcd0a521d77bdb690f701507e25c52bf3eec470 Mon Sep 17 00:00:00 2001 From: ckoegel Date: Thu, 6 Aug 2026 15:50:00 -0400 Subject: [PATCH 4/4] samples --- .../client/model/MammalAnyof.java | 50 ++++++++++--------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/MammalAnyof.java b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/MammalAnyof.java index 5b89a95468f3..ef912255d3d1 100644 --- a/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/MammalAnyof.java +++ b/samples/client/petstore/java/okhttp-gson/src/main/java/org/openapitools/client/model/MammalAnyof.java @@ -110,29 +110,33 @@ public MammalAnyof read(JsonReader in) throws IOException { Object deserialized = null; JsonElement jsonElement = elementAdapter.read(in); - JsonObject jsonObject = jsonElement.getAsJsonObject(); - - // use discriminator value for faster anyOf lookup - MammalAnyof newMammalAnyof = new MammalAnyof(); - if (jsonObject.get("className") == null) { - log.log(Level.WARNING, "Failed to lookup discriminator value for MammalAnyof as `className` was not found in the payload or the payload is empty."); - } else { - // look up the discriminator value in the field `className` - switch (jsonObject.get("className").getAsString()) { - case "Pig": - deserialized = adapterPig.fromJsonTree(jsonObject); - newMammalAnyof.setActualInstance(deserialized); - return newMammalAnyof; - case "whale": - deserialized = adapterWhale.fromJsonTree(jsonObject); - newMammalAnyof.setActualInstance(deserialized); - return newMammalAnyof; - case "zebra": - deserialized = adapterZebra.fromJsonTree(jsonObject); - newMammalAnyof.setActualInstance(deserialized); - return newMammalAnyof; - default: - log.log(Level.WARNING, String.format(java.util.Locale.ROOT, "Failed to lookup discriminator value `%s` for MammalAnyof. Possible values: Pig whale zebra", jsonObject.get("className").getAsString())); + // non-object payloads can still match a non-object anyOf schema below + if (jsonElement.isJsonObject()) { + JsonObject jsonObject = jsonElement.getAsJsonObject(); + JsonElement discriminatorElement = jsonObject.get("className"); + + // use discriminator value for faster anyOf lookup + MammalAnyof newMammalAnyof = new MammalAnyof(); + if (discriminatorElement == null || !discriminatorElement.isJsonPrimitive()) { + log.log(Level.WARNING, "Failed to lookup discriminator value for MammalAnyof as `className` is missing or is not a primitive type in the payload."); + } else { + // look up the discriminator value in the field `className` + switch (discriminatorElement.getAsString()) { + case "Pig": + deserialized = adapterPig.fromJsonTree(jsonObject); + newMammalAnyof.setActualInstance(deserialized); + return newMammalAnyof; + case "whale": + deserialized = adapterWhale.fromJsonTree(jsonObject); + newMammalAnyof.setActualInstance(deserialized); + return newMammalAnyof; + case "zebra": + deserialized = adapterZebra.fromJsonTree(jsonObject); + newMammalAnyof.setActualInstance(deserialized); + return newMammalAnyof; + default: + log.log(Level.WARNING, String.format(java.util.Locale.ROOT, "Failed to lookup discriminator value `%s` for MammalAnyof. Possible values: Pig whale zebra", discriminatorElement.getAsString())); + } } }