jsonxmlspringspring-bootroku

How do I convert my XML response from Roku Tv api to JSON?


I am trying to build an application that uses the Roku Tv ECP api commands. The specific call I am trying to make gets a list of all installed channels and returns it in XML format. Whenever I call this api, I get an error while extracting the response.

Here is how I am making my api call:

    public class ConsumeXMLResponse {

        private RestTemplate restTemplate;

        private final Logger log = LoggerFactory.getLogger(ConsumeXMLResponse.class);

        public ConsumeXMLResponse(RestTemplate restTemplate) {
            this.restTemplate = restTemplate;
        }


        public ResponseEntity<Apps> get() {
            this.restTemplate = new RestTemplateBuilder().build();
            List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
            MappingJackson2HttpMessageConverter converter = new              MappingJackson2HttpMessageConverter();
            converter.setSupportedMediaTypes(Collections.singletonList(MediaType.ALL));
            messageConverters.add(converter);
            this.restTemplate.setMessageConverters(messageConverters);

            log.info("This worked!");
            return this.restTemplate.getForEntity("http://10.0.0.229:8060/query/apps",
                    Apps.class);

        }

    }
@Controller
@RequestMapping("View")
public class RestXMLResponseController {

    private RestTemplate restTemplate;

    // Annotation
    @GetMapping(path = "/get", produces = {"application/xml"})
    public String
    getXMLOutput(Model model)
    {

        ConsumeXMLResponse response
                = new ConsumeXMLResponse(restTemplate);
        ResponseEntity<Apps> responseEntity
                = response.get();

        //Apps entity = responseEntity.getBody();
        //HttpHeaders headers = responseEntity.getHeaders();

        //model.addAttribute("xml", entity);
        //model.addAttribute("XMLheaders", headers);

        return "View";
    }

}

The object I am trying to recreate from the response:

@XmlRootElement(name = "apps")
@XmlAccessorType(XmlAccessType.FIELD)
public class Apps {

    @XmlElement(name = "app")
    private List<App> app;
}
@XmlAccessorType(XmlAccessType.FIELD)
public class App {

    @XmlAttribute(name = "id")
    public String id;

    @XmlAttribute(name = "type")
    public String type;

    @XmlAttribute(name = "version")
    public String version;

}

Here is an example of what the XML looks like when calling the api directly in postman:

<?xml version="1.0" encoding="UTF-8" ?>
<apps>
    <app id="tvinput.hdmi3" type="tvin" version="1.0.0">firestick</app>
    <app id="tvinput.hdmi1" type="tvin" version="1.0.0">Xbox</app>
    <app id="tvinput.hdmi2" type="tvin" version="1.0.0">Nintendo Switch</app>
    <app id="tvinput.dtv" type="tvin" version="1.0.0">Live TV</app>
    <app id="12" type="appl" version="5.1.120088002">Netflix</app>
    <app id="61322" type="appl" version="55.2.0">Max</app>
    <app id="13" type="appl" version="14.1.2023092022">Prime Video</app>
    <app id="2285" type="appl" version="6.75.2">Hulu</app>
    <app id="291097" type="appl" version="1.37.2023101000">Disney Plus</app>
    <app id="34376" type="appl" version="4.8.2023051800">ESPN</app>
    <app id="151908" type="appl" version="9.3.10">The Roku Channel</app>
    <app id="13842" type="appl" version="1.1.9">Vudu</app>
    <app id="46041" type="appl" version="8.96.12089">Sling TV - Live Sports, News, Shows + Freestream</app>
    <app id="45706" type="appl" version="1.0.60">Roku TV Intro</app>
    <app id="552944" type="appl" version="1.2.64">Roku Tips &amp; Tricks</app>
    <app id="562661" type="appl" version="1.2.28">Zoom TV for Kids</app>
    <app id="86398" type="appl" version="9.4.0">SYFY</app>
    <app id="593099" type="appl" version="4.11.12">Peacock TV</app>
    <app id="41468" type="appl" version="3.0.2">Tubi - Free Movies &amp; TV</app>
    <app id="31440" type="appl" version="8.8.202310302">Paramount Plus</app>
    <app id="dev" type="appl" version="1.0.1">MovieBoxPro</app>
</apps>

I tried to call the api that returns an XML response, then convert it to a Java object that maps to Apps the class. The application throws an error whenever it tries to parse the response.

Here is the error:

2023-11-06T17:06:46.430-05:00 ERROR 76726 --- [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.web.client.RestClientException: Error while extracting response for type [class com.TelevisionRemote.SmartTelevisionRemote.component.Apps] and content type [text/xml;charset="utf-8"]] with root cause

com.fasterxml.jackson.core.JsonParseException: Unexpected character ('<' (code 60)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
 at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 2]
    at com.fasterxml.jackson.core.JsonParser._constructError(JsonParser.java:2418) ~[jackson-core-2.14.2.jar:2.14.2]
    at com.fasterxml.jackson.core.base.ParserMinimalBase._reportError(ParserMinimalBase.java:749) ~[jackson-core-2.14.2.jar:2.14.2]
    at com.fasterxml.jackson.core.base.ParserMinimalBase._reportUnexpectedChar(ParserMinimalBase.java:673) ~[jackson-core-2.14.2.jar:2.14.2]
    at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._handleUnexpectedValue(UTF8StreamJsonParser.java:2784) ~[jackson-core-2.14.2.jar:2.14.2]
    at com.fasterxml.jackson.core.json.UTF8StreamJsonParser._nextTokenNotInObject(UTF8StreamJsonParser.java:907) ~[jackson-core-2.14.2.jar:2.14.2]
    at com.fasterxml.jackson.core.json.UTF8StreamJsonParser.nextToken(UTF8StreamJsonParser.java:793) ~[jackson-core-2.14.2.jar:2.14.2]
    at com.fasterxml.jackson.databind.ObjectReader._initForReading(ObjectReader.java:357) ~[jackson-databind-2.14.2.jar:2.14.2]
    at com.fasterxml.jackson.databind.ObjectReader._bindAndClose(ObjectReader.java:2095) ~[jackson-databind-2.14.2.jar:2.14.2]
    at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:1481) ~[jackson-databind-2.14.2.jar:2.14.2]
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:395) ~[spring-web-6.0.6.jar:6.0.6]
    at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:354) ~[spring-web-6.0.6.jar:6.0.6]
    at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:103) ~[spring-web-6.0.6.jar:6.0.6]
    at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:1132) ~[spring-web-6.0.6.jar:6.0.6]
    at org.springframework.web.client.RestTemplate$ResponseEntityResponseExtractor.extractData(RestTemplate.java:1115) ~[spring-web-6.0.6.jar:6.0.6]
    at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:865) ~[spring-web-6.0.6.jar:6.0.6]
    at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:764) ~[spring-web-6.0.6.jar:6.0.6]
    at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:405) ~[spring-web-6.0.6.jar:6.0.6]
    at com.TelevisionRemote.SmartTelevisionRemote.controller.ConsumeXMLResponse.get(ConsumeXMLResponse.java:38) ~[classes/:na]
    at com.TelevisionRemote.SmartTelevisionRemote.controller.RestXMLResponseController.getXMLOutput(RestXMLResponseController.java:28) ~[classes/:na]
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) ~[na:na]
    at java.base/java.lang.reflect.Method.invoke(Method.java:578) ~[na:na]
    at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:207) ~[spring-web-6.0.6.jar:6.0.6]
    at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:152) ~[spring-web-6.0.6.jar:6.0.6]
    at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117) ~[spring-webmvc-6.0.6.jar:6.0.6]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:884) ~[spring-webmvc-6.0.6.jar:6.0.6]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797) ~[spring-webmvc-6.0.6.jar:6.0.6]
    at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-6.0.6.jar:6.0.6]
    at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1081) ~[spring-webmvc-6.0.6.jar:6.0.6]
    at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974) ~[spring-webmvc-6.0.6.jar:6.0.6]
    at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011) ~[spring-webmvc-6.0.6.jar:6.0.6]
    at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.0.6.jar:6.0.6]
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:705) ~[tomcat-embed-core-10.1.5.jar:6.0]
    at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.0.6.jar:6.0.6]
    at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:814) ~[tomcat-embed-core-10.1.5.jar:6.0]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:223) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53) ~[tomcat-embed-websocket-10.1.5.jar:10.1.5]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.0.6.jar:6.0.6]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.6.jar:6.0.6]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.0.6.jar:6.0.6]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.6.jar:6.0.6]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.0.6.jar:6.0.6]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.0.6.jar:6.0.6]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:185) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:158) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:177) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:542) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:119) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:357) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:400) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:859) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1734) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) ~[tomcat-embed-core-10.1.5.jar:10.1.5]
    at java.base/java.lang.Thread.run(Thread.java:1623) ~[na:na]


Solution

  • Turns out that adding @JacksonXmlElementWrapper to my Apps class on the app list attribute solved the issue.

    @JacksonXmlElementWrapper(useWrapping = false)
    private List<App> app;
    

    I also did not need the message converter.

    public Apps getXMLOutput() {
    
        ResponseEntity<Apps> responseEntity
                = this.restTemplate.getForEntity("http://10.0.0.229:8060/query/apps",
                Apps.class);
    
        Apps apps = responseEntity.getBody();
    
        log.info(apps.toString());
        return appsRepository.save(apps);
    }
    

    The result in postman:

    {
        "id": 1,
        "app": [
            {
                "id": "tvinput.hdmi3",
                "type": "tvin",
                "version": "1.0.0"
            },
            {
                "id": "tvinput.hdmi1",
                "type": "tvin",
                "version": "1.0.0"
            },
            {
                "id": "tvinput.hdmi2",
                "type": "tvin",
                "version": "1.0.0"
            },
            {
                "id": "tvinput.dtv",
                "type": "tvin",
                "version": "1.0.0"
            },
            {
                "id": "12",
                "type": "appl",
                "version": "5.1.120088002"
            },
            {
                "id": "61322",
                "type": "appl",
                "version": "55.2.0"
            },
            {
                "id": "13",
                "type": "appl",
                "version": "14.1.2023092022"
            },
            {
                "id": "2285",
                "type": "appl",
                "version": "6.75.2"
            },
            {
                "id": "291097",
                "type": "appl",
                "version": "1.37.2023101000"
            },
            {
                "id": "34376",
                "type": "appl",
                "version": "4.8.2023051800"
            },
            {
                "id": "151908",
                "type": "appl",
                "version": "9.3.10"
            },
            {
                "id": "13842",
                "type": "appl",
                "version": "1.1.9"
            },
            {
                "id": "46041",
                "type": "appl",
                "version": "8.97.12290"
            },
            {
                "id": "45706",
                "type": "appl",
                "version": "1.0.60"
            },
            {
                "id": "552944",
                "type": "appl",
                "version": "1.2.64"
            },
            {
                "id": "562661",
                "type": "appl",
                "version": "1.2.28"
            },
            {
                "id": "86398",
                "type": "appl",
                "version": "9.4.0"
            },
            {
                "id": "593099",
                "type": "appl",
                "version": "4.11.12"
            },
            {
                "id": "41468",
                "type": "appl",
                "version": "3.0.2"
            },
            {
                "id": "31440",
                "type": "appl",
                "version": "8.8.202310302"
            },
            {
                "id": "dev",
                "type": "appl",
                "version": "1.0.1"
            }
        ]
    }