[BUG] [Java] [withXml] xml name not used for items wrapped in an array
Bug Report Checklist
- [x] Have you provided a full/minimal spec to reproduce the issue?
- [ ] Have you validated the input using an OpenAPI validator (example)?
- [x] Have you tested with the latest master to confirm the issue still exists?
- [x] Have you searched for related issues/PRs?
- [x] What's the actual output vs expected output?
- [ ] [Optional] Sponsorship to speed up the bug fix or feature request (example)
Description
I have the following swagger which defined an array of message object.
swagger: "2.0"
definitions:
Status:
type: "object"
properties:
messages:
type: "array"
items:
$ref: "#/definitions/Message"
xml:
name: message
xml:
name: messages
wrapped: true
i want to generate java code which matches the xml below. Note that the message array (
<status>
<messages>
<message>
</message>
<message>
</message>
</messages>
</status>
The problem is in the generation of Status java class which contains the messages list :
public class Status implements Serializable {
@XmlElement(name = "messages") // problem here
@XmlElementWrapper(name = "messages")
private List<Message> messages = null;
}
The value of attribut name in XmlElement annotation should be message (without s). The generator ignores the name tag under items tag in the swagger file.
openapi-generator version
5.1.0
OpenAPI declaration file content or url
I'm not sure if the bug is located in this template file but the following line in pojo.mustache seems strange; The value name uses the same variable 'xmlName' for both @XmlElement and @XmlElementWrapper.
// Is a container wrapped={{isXmlWrapped}}
{{#items}}
// items.name={{name}} items.baseName={{baseName}} items.xmlName={{xmlName}} items.xmlNamespace={{xmlNamespace}}
// items.example={{example}} items.type={{dataType}}
@XmlElement({{#xmlNamespace}}namespace="{{xmlNamespace}}", {{/xmlNamespace}}name = "{{#xmlName}}{{xmlName}}{{/xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}")
{{/items}}
{{#isXmlWrapped}}
@XmlElementWrapper({{#xmlNamespace}}namespace="{{xmlNamespace}}", {{/xmlNamespace}}name = "{{#xmlName}}{{xmlName}}{{/xmlName}}{{^xmlName}}{{baseName}}{{/xmlName}}")
{{/isXmlWrapped}}
Generation Details
Steps to reproduce
Related issues/PRs
Suggest a fix
Can you please try {{{items.xmlName}}} to see if fixes the issue for you?
Hi,
I tried with {{{items.xmlName}}} but it doesn't work. I'm done other tests and in fact, it doesn't work in my case because I use $ref in items. If I write 'type: object',the original pojo.mustache works as expected, the {{xmlName}} has the value "message"
Status:
type: "object"
properties:
messages:
type: "array"
items:
$ref: "#/definitions/Message" # Don't work. Generates @XmlElement(name = "messages")
#type: object # Work as expected. Generates @XmlElement(name = "message")
xml:
name: message
xml:
name: messages
wrapped: true
xml:
name: status
Hi @wing328,
I think the problem for my case is in DefaultCodegen.java file (extract below). The itemName variable is deduced from property name (with my example is equals to "messages"). But the array type references a type Message in the swagger file. So for this specific case (xmlWrapped = true and items.$ref != null ), check if items.$ref.xml.name is set. If yes, override itenName with this value.
public CodegenProperty fromProperty(String name, Schema p) {
....
if (p.getXml() != null) {
property.isXmlWrapped = p.getXml().getWrapped() == null ? false : p.getXml().getWrapped();
property.xmlPrefix = p.getXml().getPrefix();
property.xmlNamespace = p.getXml().getNamespace();
property.xmlName = p.getXml().getName();
}
// handle inner property
String itemName = null;
if (p.getExtensions() != null && p.getExtensions().get("x-item-name") != null) {
itemName = p.getExtensions().get("x-item-name").toString();
}
if (itemName == null) {
itemName = property.name;
}
ArraySchema arraySchema = (ArraySchema) p;
Schema innerSchema = unaliasSchema(getSchemaItems(arraySchema), importMapping);
CodegenProperty cp = fromProperty(itemName, innerSchema);
updatePropertyForArray(property, cp);
}
In my IDE, I modified on the fly the itemName value, and it works as expected.
I investigated a little more on my side. I think I found where there is a bug. And I can prove it with 2 unit tests below. Unfortunately, the fix is harder for me to find even I think I localized the lines of code in trouble.
Status:
type: "object"
properties:
messages:
type: "array"
items:
$ref: "#/definitions/MessageWithOneProperty" # Don't work if Message has a property
#$ref: "#/definitions/MessageWithNoProperty" # works as expected
xml:
name: message
xml:
name: messages
wrapped: true
xml:
name: status
MessageWithOneProperty:
type: "object"
properties:
id:
type: "string"
xml:
name: message
MessageWithNoProperty:
type: "object"
xml:
name: message
// Test works
@Test(description = "test models with xml attributes with an ObjectSchema without properties which is wrapped by an array")
public void modelWithWrappedXmlOfObjectReferenceWithNoPropertiesTest() {
final Schema schema = new ObjectSchema().addProperties("array", new ArraySchema().items(new Schema<>().$ref("#/components/schemas/MyObject"))
.xml(new XML().wrapped(true)
.name("xmlArray")));
final Schema schemaMyObjectWithNoProperties = new ObjectSchema().xml(new XML().name("myObject"));
final DefaultCodegen codegen = new JavaClientCodegen();
final OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("sample", schema);
openAPI.getComponents()
.addSchemas("MyObject", schemaMyObjectWithNoProperties);
codegen.setOpenAPI(openAPI);
final CodegenModel cm = codegen.fromModel("sample", schema);
Assert.assertEquals(cm.vars.size(), 1);
final List<CodegenProperty> vars = cm.vars;
final CodegenProperty property1 = vars.get(0);
Assert.assertEquals(property1.baseName, "array");
Assert.assertEquals(property1.dataType, "List<Object>");
Assert.assertEquals(property1.name, "array");
Assert.assertEquals(property1.defaultValue, "new ArrayList<Object>()");
Assert.assertEquals(property1.baseType, "List");
Assert.assertTrue(property1.isContainer);
Assert.assertTrue(property1.isXmlWrapped);
Assert.assertEquals(property1.xmlName, "xmlArray");
Assert.assertNotNull(property1.items);
final CodegenProperty items = property1.items;
Assert.assertEquals(items.xmlName, "myObject");
Assert.assertEquals(items.baseName, "array");
}
// Test fails
@Test(description = "test models with xml attributes with an ObjectSchema with one property which is wrapped by an array")
public void modelWithWrappedXmlOfObjectReferenceWithOnePropertiesTest() {
final Schema schema = new ObjectSchema().addProperties("array", new ArraySchema().items(new Schema<>().$ref("#/components/schemas/MyObject"))
.xml(new XML().wrapped(true)
.name("xmlArray")));
final Schema schemaMyObjectWithOneProperties = new ObjectSchema().xml(new XML().name("myObject"))
.addProperties("id", new IntegerSchema().format(SchemaTypeUtil.INTEGER64_FORMAT));
final DefaultCodegen codegen = new JavaClientCodegen();
final OpenAPI openAPI = TestUtils.createOpenAPIWithOneSchema("sample", schema);
openAPI.getComponents()
.addSchemas("MyObject", schemaMyObjectWithOneProperties);
codegen.setOpenAPI(openAPI);
final CodegenModel cm = codegen.fromModel("sample", schema);
Assert.assertEquals(cm.vars.size(), 1);
final List<CodegenProperty> vars = cm.vars;
final CodegenProperty property1 = vars.get(0);
Assert.assertEquals(property1.baseName, "array");
Assert.assertEquals(property1.dataType, "List<MyObject>"); // Diff here with the test above but I can explain why
Assert.assertEquals(property1.name, "array");
Assert.assertEquals(property1.defaultValue, "new ArrayList<MyObject>()"); // Diff here with the test above but I can explain why
Assert.assertEquals(property1.baseType, "List");
Assert.assertTrue(property1.isContainer);
Assert.assertTrue(property1.isXmlWrapped);
Assert.assertEquals(property1.xmlName, "xmlArray");
Assert.assertNotNull(property1.items);
final CodegenProperty items = property1.items;
Assert.assertEquals(items.xmlName, "myObject"); // AssertionError: expected [myObject] but found [null]
Assert.assertEquals(items.baseName, "array");
}
below an extraction of the code concerned by this problem. I added comments to explain my understanding.
class DefaultCodegen {
public CodegenProperty fromProperty(String name, Schema p) {
...
// handle inner property
String itemName = null;
if (p.getExtensions() != null && p.getExtensions().get("x-item-name") != null) {
itemName = p.getExtensions().get("x-item-name").toString();
}
if (itemName == null) {
itemName = property.name;
}
ArraySchema arraySchema = (ArraySchema) p;
Schema innerSchema = unaliasSchema(getSchemaItems(arraySchema), importMapping);
CodegenProperty cp = fromProperty(itemName, innerSchema);
updatePropertyForArray(property, cp);
}
}
class Modelutils {
public static Schema unaliasSchema(final OpenAPI openAPI, final Schema schema, final Map<String, String> importMappings) {
...
final Schema ref = allSchemas.get(simpleRef);
....
} else if (isObjectSchema(ref)) { // model
// from my point of view, the bug is here. If ref value has a property (in my example above, it's the Message object), it returns the parent object (Status in my example) instead of Message object. I tried to change 'return schema' by 'return ref' but about 50 unit tests failed. And I don't understand why if properties are not empty, we return schema value and not ref value.
if (ref.getProperties() != null && !ref.getProperties().isEmpty()) { // has at least one property
return schema;
} else { // free form object (type: object)
return unaliasSchema(openAPI, allSchemas.get(ModelUtils.getSimpleRef(schema.get$ref())), importMappings);
}
}
}
}
https://github.com/OpenAPITools/openapi-generator/issues/9371#issue-871223420 with
xml:
wrapped: false
I would expect the following output:
<status>
<message>
</message>
<message>
</message>
</status>
but it has no effect.
version 6.2.1
@RomainPruvostMHH @wing328
I'm done other tests and in fact, it doesn't work in my case because I use $ref in items.
I was able to reproduce this behavior on $refd objects, while items: section containing any other types works fine. Interestingly enough, you copy the schema definition over and it will also work as expected:
Status:
type: "object"
properties:
messages:
type: "array"
items:
# $ref: "#/definitions/MessageWithOneProperty" ↓ instead of $ref, copying in the definition
type: "object"
properties:
id:
type: "string"
xml:
name: message
xml:
name: messages
wrapped: true
xml:
name: status
I'm not an OAS spec expert, but i have been scratching my head about this sentence in the spec before:
This object cannot be extended with additional properties and any properties added SHALL be ignored
which is found in the v3 spec: https://spec.openapis.org/oas/v3.0.0#reference-object
Also, https://swagger.io/docs/specification/using-ref/ clearly states that:
Any sibling elements of a $ref are ignored.
So is it possible that this behavior is completely expected for OAS v3? All the above strongly suggests that.
However, notably, the example in this issue entry uses Swagger 2.0 which does not include this sentence in the spec: https://swagger.io/specification/v2/#reference-object
So is this "only" an issue of the openapitools not correctly implementing Swagger v2 here or may this be expected behavior as well (which was however not clearly formulated in the spec)?
Same issue here. Java client generator version 7.7.0
Found a bit of an ugly fix: Assume you want to have the following object spec:
type: object
xml:
name: parent
properties:
children:
type: array
items:
$ref: './child.yml'
xml:
wrapped: true
and expect an xml structure like:
<parent>
<children>
<child></child>
</children>
</parent>
Then a dirty fix would be:
type: object
xml:
name: parent
properties:
children:
type: array
items:
type: object
xml:
name: child
properties:
age: integer
xml:
wrapped: true
which would generate a model class named "ParentChildListInner". To successfully map this generated class to "Child" add the following config to the OpenAPI generator:
<inlineSchemaNameMappings>parent_childList_inner=Child</inlineSchemaNameMappings>
Note that you will have to make the generator generate the class "Child" somehow. I did so by adding an endpoint for fetching a single child by id, referencing the spec in child.yml