openapi-generator icon indicating copy to clipboard operation
openapi-generator copied to clipboard

[BUG] [Java] [withXml] xml name not used for items wrapped in an array

Open RomainPruvostMHH opened this issue 4 years ago • 6 comments

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 ( element) wrapped message items and each item are named .

	<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

RomainPruvostMHH avatar Apr 29 '21 16:04 RomainPruvostMHH

Can you please try {{{items.xmlName}}} to see if fixes the issue for you?

wing328 avatar Apr 30 '21 06:04 wing328

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

RomainPruvostMHH avatar May 01 '21 07:05 RomainPruvostMHH

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.

RomainPruvostMHH avatar May 02 '21 09:05 RomainPruvostMHH

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);
		}
	}
  }
}

RomainPruvostMHH avatar May 04 '21 09:05 RomainPruvostMHH

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

gfiedler avatar Jan 16 '24 16:01 gfiedler

@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)?

Philzen avatar May 22 '24 00:05 Philzen

Same issue here. Java client generator version 7.7.0

alexander793 avatar Jan 23 '25 13:01 alexander793

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

alexander793 avatar Jan 23 '25 13:01 alexander793