FasterXML / jackson-dataformat-xml

Extension for Jackson JSON processor that adds support for serializing POJOs as XML (and deserializing from XML) as an alternative to JSON
Apache License 2.0
561 stars 221 forks source link

java.lang.IllegalArgumentException: Cannot deserialize value of type `java.util.ArrayList<RetInfArryDTO> from Object value (token `JsonToken.START_OBJECT`) #630

Closed oooooooz closed 4 months ago

oooooooz commented 5 months ago

Search before asking

Describe the bug

I use jackson-dataformat-xml to deserialize XML , but it throws exception : Exception in thread "main" java.lang.IllegalArgumentException: Cannot deserialize value of type java.util.ArrayList<RetInfArryDTO> from Object value (token JsonToken.START_OBJECT) at [Source: UNKNOWN; line: -1, column: -1] (through reference chain: AnRspDTO["SysHead"]->SysHeadRspDTO["RetInfArry"])

It look like thak jackson unrecognize this type of XML format data , could not treat the array in XML as real array to deserialize , but I think it is a kind of common XML data which contain array type, isn't it ?

I tried annotation like @JsonDeserialize(contentAs = AddrInfArryDTO.class), @JsonAlias({ "IdInfArry", "IdInfArry.array" }), but it did not work for me !

Q1: Is anyone know how to solve this problem? Q2: If I must use custom JsonDeserializer to solve , how can I get the elementType below ?could not get it from both jsonParser and deserializationContext, and i don't want to write hard code , give exactly elementType here, because too must DTO to be deserialized , write a general custom JsonDeserializer is very necessary!


 @Override
    public List<T> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
            throws IOException, JsonProcessingException {
        List<T> list = new ArrayList<>();
        ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec();
        JsonNode node = jsonParser.readValueAsTree();

        Iterator<JsonNode> elements = node.elements();
            while (elements.hasNext()) {
                JsonNode element = elements.next();
                T obj = deserializationContext.readValue(element.traverse(), elementType);
                list.add(obj);
        }

        return list;
    }

Here below is my XML data to be deserialized and its DTO object :

(1)XML data like this :

<?xml version="1.0" encoding="UTF-8"?>
<service>
  <SysHead>
    <SvcCd>1234</SvcCd>
    <ScnCd>1245</ScnCd>
     <RetInfArry type="array">
        <array>
                    <RetCd>0000</RetCd>
                    <RetMsg>succ</RetMsg>
         </array>
      </RetInfArry>`
  <BODY>
    <CrpnScop></CrpnScop>
    <IdInfArry type="array">
        <array>
                        <IdentSeqNo>12345</IdentSeqNo>
                        <IdentTp>22</IdentTp>
        </array>
        <array>
                        <IdentSeqNo>1234</IdentSeqNo>
                        <IdentTp>22</IdentTp>
        </array>
     </IdInfArry>
    <AddrInfArry type="array">
      <array>
                      <CustAddr>1234</CustAddr>
                      <AddrTp>12</AddrTp>
       </array>
      <array>
                      <CustAddr>1234</CustAddr>
                      <AddrTp>12</AddrTp>
       </array>
    </AddrInfArry>
    </BODY>
</service>

(2)Here is my DTO , XML deserialize object :

//@JsonProperty work fine, not need to use @JacksonXmlProperty 

@lombok.Data
public class AnRspDTO {
  @JsonProperty("SysHead")
    private SysHeadRspDTO sysHead;

   @JsonProperty("BODY")
    private RspBodyDTO body;

@lombok.Data
public static class RspBodyDTO {

    @JsonProperty("CrpnScop")
     private String crpnScop = "";

    @JsonProperty("IdInfArry")
     private List<IdInfArryDTO> idInfArry = Collections.emptyList();

     @JsonProperty("AddrInfArry")
     private List<AddrInfArryDTO> addrInfArry = Collections.emptyList();

@lombok.Data
public static class IdInfArryDTO {

    @JsonProperty("IdentSeqNo")
     private String identSeqNo = "";

    @JsonProperty("IdentTp")
     private String identTp = "";

}

@lombok.Data
public static class AddrInfArryDTO {

     @JsonProperty("CustAddr")
      private String custAddr = "";

      @JsonProperty("AddrTp")
       private String addrTp = "";

}

}

@lombok.Data
public class SysHeadRspDTO {
  @JsonProperty("SvcCd")
  private String svcCd

  @JsonProperty("ScnCd")
  private String scnCd

  @JsonProperty("RetInfArry")
   private List<RetInfArryDTO> retInfArry = Collections.emptyList();

@lombok.Data
public static class RetInfArryDTO{

    @JsonProperty("RetCd")
    private String retCd;

    @JsonProperty("RetMsg")
    private String retMsg;
}
}

Version Information

pom dependency , current steady version 2.16.0 could not solve the problem too

<dependency>
            <groupId>com.fasterxml.jackson.dataformat</groupId>
            <artifactId>jackson-dataformat-xml</artifactId>
<!--            <version>2.16.0</version>-->
            <version>2.12.3</version>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.datatype</groupId>
            <artifactId>jackson-datatype-jsr310</artifactId>
<!--            <version>2.16.0</version>-->
            <version>2.12.3</version>
        </dependency>

Reproduction

<-- Any of the following

  1. Brief code sample/snippet: include here in preformatted/code section
  2. Longer example stored somewhere else (diff repo, snippet), add a link
  3. Textual explanation: include here -->

public static T decode(String msg, Class entityType) { ObjectMapper objectMapper = getObjectMapper(); try { return objectMapper.readValue(msg, entityType); } catch (Exception ex) { throw new RuntimeException(ex); } }

public static T convert(Object src, Class dstType) { ObjectMapper objectMapper = getObjectMapper(); return objectMapper.convertValue(src, dstType); }

public static ObjectMapper getObjectMapper(){

XmlMapper.Builder builder = XmlMapper.builder()
            .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .configure(ToXmlGenerator.Feature.WRITE_XML_DECLARATION,true)
            .configure(MapperFeature.USE_ANNOTATIONS, enableAnno);

return builder.build();

}

public static void main(String[] args) { String xmldata=getXml()// Map map =decode(xmldata,Map.class); AnRspDTO dtoObj = convert(map,RspBodyDTO.class); Assert.notNull(dtoObj) }



### Expected behavior

_No response_

### Additional context

_No response_
cowtowncoder commented 5 months ago

Wrong repo, will transfer.

cowtowncoder commented 5 months ago

Ok, there are quite a few challenges for reproduction here.

For starters, when testing, PLEASE NEVER DISABLE DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES. Doing so will regularly hide real problems. You can disable that for production code, but for testing, don't.

Second: although use of Lombok is fine with Jackson, and should work, test reproductions should not use Lombok since its bytecode post-processing makes it difficult to know what is happening. None of test in Jackson repos can use Lombok as a consequence. I assume it is possible to change reproductions to avoid it, either by getting processed class definitions, or convert manually.

Third: Jackson does not actually support deserialization for all kinds of XML representations: it specifically aims to support use case of "reading what it writes": so, if you start with Java objects, serialize as XML, it should also be able to read XML back into same Java objects. So to troubleshoot deserialization problems, it is usually best to start by creating Java objects, serializing as XML, and looking at how this differs from XML you'd like to read. If you can change annotations or class definitions to produce that XML structure, you should be able to read it back. If not, you most likely cannot.

Fourth: your attempt to read XML as Map, then convert to Java object is very unlikely to work, at all. XML as a data format is challenging to map to/from Java Objects, and requires class definitions -- so trying to read as java.util.Map will lose information and not allow conversion. This is different from JSON, one difference being that JSON has native Array and Object type difference; XML does not (it only has Elements, which are more similar to. JSON Objects than Arrays). To support Array/Collection types, readValue() has to bind to actual Java class, not JDK container types like Maps or Collections. So for things to work, you will need to try to make XmlMapper.readValue() do all the work: mapper.convertValue() is not likely to work (but I am also not sure it is even being attempted: I am not sure I follow intended logic).

I hope this helps.

oooooooz commented 5 months ago

OK ,thank you very much, let me know more about jackson 👍 i here there some historical items burden, use Lombok , the XML format ,etc.. just like dancing in fetters. I just test on them and not being conscious of some problems you describe.

It look like that the encoding of XML declaration is UTF-8 , with hard code when serialization , and ToXmlGenerator cannot be overrided in 2.12.3 , but i need another encoding declaration more than UTF-8, how could i do ? Is jackson use this encoding when serialize? @cowtowncoder

cowtowncoder commented 5 months ago

If you want to use some other encoding, you will need to create XMLStreamWriter from Stax implementation (like Woodstox), call its writeStartDocument() (version that takes encoding argument), and then use XmlMapper.writeValue(XMLStreamWriter, Object) to serialize using that XMLStreamWriter.

oooooooz commented 5 months ago

@cowtowncoder OK, What's up with [#355 ] and [#324 ] now , Is there any solution? only root element need namespace and xsi:type ,thank you

cowtowncoder commented 5 months ago

What do issues say? If there is progress, solution, there will be comments. Absence of that, no progress.

355 in particular is closed: there is no defect, although users are unhappy about having to include namespace with all namespace declarations.

So perhaps something could be figured for some kind of namespace defaulting... I am open to suggestions.

324 would be great to support, but as things are it is not easy to do: partly since there is no way to specify namespace for Type Ids (via @JsonTypeInfo).

Although... perhaps, just perhaps, there could be a feature to force simple name of "xsi:type" to be automatically converted into qualified name with proper namespace. This would have to be ToXmlGenerator.Feature. Actually, it should probably be a feature that would handle all "xsi:*" cases by splitting property name into local name, namespace URI; and would then handle xsi:type. I'll add a note there.

cowtowncoder commented 5 months ago

Added my notes on #324: maybe I'll play with that later tonight.

cowtowncoder commented 5 months ago

Ok did fix #324 but also filed a follow-up for deserialization side (#634) now that generation of xsi:type is possible with 2.17.