Devolviendo el objeto JSON como respuesta en Spring Boot

85

Tengo un RestController de muestra en Spring Boot:

@RestController
@RequestMapping("/api")
class MyRestController
{
    @GetMapping(path = "/hello")
    public JSONObject sayHello()
    {
        return new JSONObject("{'aa':'bb'}");
    }
}

Estoy usando la biblioteca JSON org.json

Cuando presiono API /hello, obtengo una excepción que dice:

Servlet.service () para servlet [dispatcherServlet] en contexto con la ruta [] arrojó una excepción [El procesamiento de la solicitud falló; La excepción anidada es java.lang.IllegalArgumentException: No se encontró un convertidor para el valor de retorno del tipo: class org.json.JSONObject] con la causa raíz.

java.lang.IllegalArgumentException: No se encontró ningún convertidor para el valor de retorno del tipo: class org.json.JSONObject

¿Cual es el problema? ¿Alguien puede explicar qué está sucediendo exactamente?

iwekesi
fuente
Jackson no puede convertir JSONObject a json.
Pau
Ok, lo entiendo. ¿Qué se puede hacer para arreglar esto?
iwekesi
1
Quiero que la respuesta se construya sobre la marcha. No quiero crear clases específicas para cada respuesta.
iwekesi
2
Podría ser mejor que su método regrese como String. Además, también puede agregar la anotación @ResponseBody al método, esto manejará su respuesta según lo solicitado :-)@GetMapping(path = "/hello") @ResponseBody public String sayHello() {return"{'aa':'bb'}";}
vegaasen
@vegaasen, ¿puedes elaborar un poco sobre ResponseBody?
iwekesi

Respuestas:

103

Como está utilizando la web Spring Boot, la dependencia de Jackson es implícita y no tenemos que definirla explícitamente. Puede verificar la dependencia de Jackson en su pom.xmlen la pestaña de jerarquía de dependencia si usa eclipse.

Y como ha anotado, @RestControllerno es necesario realizar una conversión json explícita. Simplemente devuelva un POJO y el serializador jackson se encargará de convertir a json. Es equivalente a usar @ResponseBodycuando se usa con @Controller. En lugar de colocar @ResponseBodyen cada método de controlador, colocamos en lugar @RestControllerde vainilla @Controllery, @ResponseBodyde forma predeterminada, se aplica a todos los recursos en ese controlador.
Consulte este enlace: https://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-responsebody

El problema al que se enfrenta es porque el objeto devuelto (JSONObject) no tiene getter para ciertas propiedades. Y su intención no es serializar este JSONObject, sino serializar un POJO. Así que devuelve el POJO.
Consulte este enlace: https://stackoverflow.com/a/35822500/5039001

Si desea devolver una cadena serializada json, simplemente devuelva la cadena. Spring usará StringHttpMessageConverter en lugar del convertidor JSON en este caso.

prem kumar
fuente
si la cadena json es lo que desea devolver de java, puede devolver una cadena si ya está serializada en json. No es necesario convertir string a json y json de nuevo a string.
prem kumar
5
Si desea devolver un conjunto de pares nombre-valor sin una estructura rígida en tiempo de compilación, puede devolver Map<String,Object>un Propertiesobjeto o un objeto
Vihung
@prem kumar pregunta aleatoria: ¿qué quieres decir con 'en lugar de vanilla Controller y ResponseBody'? ¿Qué hay de vainilla aquí?
Orkun Ozen
Me refiero a un controlador habitual y con una anotación ResponseBody colocada en cada método de solicitud.
prem kumar
66

La razón por la que su enfoque actual no funciona es porque Jackson se usa de forma predeterminada para serializar y deserializar objetos. Sin embargo, no sabe cómo serializar JSONObject. Si desea crear una estructura JSON dinámica, puede usar Map, por ejemplo:

@GetMapping
public Map<String, String> sayHello() {
    HashMap<String, String> map = new HashMap<>();
    map.put("key", "value");
    map.put("foo", "bar");
    map.put("aa", "bb");
    return map;
}

Esto conducirá a la siguiente respuesta JSON:

{ "key": "value", "foo": "bar", "aa": "bb" }

Esto es un poco limitado, ya que puede resultar un poco más difícil agregar objetos secundarios. Sin embargo, Jackson tiene su propio mecanismo, usando ObjectNodey ArrayNode. Para usarlo, debe realizar un autowire ObjectMapperen su servicio / controlador. Entonces puedes usar:

@GetMapping
public ObjectNode sayHello() {
    ObjectNode objectNode = mapper.createObjectNode();
    objectNode.put("key", "value");
    objectNode.put("foo", "bar");
    objectNode.put("number", 42);
    return objectNode;
}

Este enfoque le permite agregar objetos secundarios, matrices y usar todos los tipos.

g00glen00b
fuente
2
¿Qué es Mapper aquí?
iwekesi
1
@iwekesi, ese es el Jackson ObjectMapperque debe conectar automáticamente (consulte el párrafo anterior a mi último fragmento de código).
g00glen00b
1
¡Es asombroso saber que uno tiene que llegar tan lejos para producir objetos JSON significativos! También es triste que Pivotal no haga ningún esfuerzo en absoluto ( spring.io/guides/gs/actuator-service ) para señalar estas limitaciones. ¡Afortunadamente, tenemos SO! ;)
cogitoergosum
La importación java.util.HashMap no se puede resolver
Hikaru Shindo
@HikaruShindo java.util.HashMapes una funcionalidad central de Java desde Java 1.2.
g00glen00b
43

Puede devolver una respuesta como Stringlo sugiere @vagaasen o puede usar el ResponseEntityObjeto proporcionado por Spring como se muestra a continuación. De esta manera, también puede devolver lo Http status codeque es más útil en la llamada de servicio web.

@RestController
@RequestMapping("/api")
public class MyRestController
{

    @GetMapping(path = "/hello", produces=MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<Object> sayHello()
    {
         //Get data from service layer into entityList.

        List<JSONObject> entities = new ArrayList<JSONObject>();
        for (Entity n : entityList) {
            JSONObject entity = new JSONObject();
            entity.put("aa", "bb");
            entities.add(entity);
        }
        return new ResponseEntity<Object>(entities, HttpStatus.OK);
    }
}
Sangam Belose
fuente
1
Si agrego JSONObject en entidades, nuevamente me está dando una excepción similar
iwekesi
@Sangam su respuesta me ayudó con otro problema relacionado con jackson-dataformat-xml
divine
¡Esta fue una gran ayuda! ¡Gracias!
jones-chris
1
Me pregunto por qué esta respuesta no recibe más votos a favor. Yo también soy nuevo en Spring, así que no sé si esta es una buena práctica de ingeniería de software. Dicho esto, esta respuesta realmente me ayudó. Sin embargo, tuve muchos problemas con a JSONObject, pero como Spring usa Jackson, lo cambié a a HashMapy luego el código que leí en esta respuesta funcionó.
Melvin Roest
2
+1 por sugerir MediaType.APPLICATION_JSON_VALUE, ya que garantiza que el resultado producido se analice como json, no xml, como puede suceder si no lo define
Sandeep Mandori
11

también puedes usar un mapa de hash para esto

@GetMapping
public HashMap<String, Object> get() {
    HashMap<String, Object> map = new HashMap<>();
    map.put("key1", "value1");
    map.put("results", somePOJO);
    return map;
}
maximus
fuente
6
@RequestMapping("/api/status")
public Map doSomething()
{
    return Collections.singletonMap("status", myService.doSomething());
}

PD. Funciona solo por 1 valor

Dmitry
fuente
3

utilizar ResponseEntity<ResponseBean>

Aquí puede usar ResponseBean o Any Java Bean como desee para devolver su respuesta de API y es la mejor práctica. He usado Enum como respuesta. devolverá el código de estado y el mensaje de estado de la API.

@GetMapping(path = "/login")
public ResponseEntity<ServiceStatus> restApiExample(HttpServletRequest request,
            HttpServletResponse response) {
        String username = request.getParameter("username");
        String password = request.getParameter("password");

        loginService.login(username, password, request);
        return new ResponseEntity<ServiceStatus>(ServiceStatus.LOGIN_SUCCESS,
                HttpStatus.ACCEPTED);
    }

para respuesta ServiceStatus o (ResponseBody)

    public enum ServiceStatus {

    LOGIN_SUCCESS(0, "Login success"),

    private final int id;
    private final String message;

    //Enum constructor
    ServiceStatus(int id, String message) {
        this.id = id;
        this.message = message;
    }

    public int getId() {
        return id;
    }

    public String getMessage() {
        return message;
    }
}

Spring REST API debe tener la siguiente clave en respuesta

  1. Código de estado
  2. Mensaje

obtendrá la respuesta final a continuación

{

   "StatusCode" : "0",

   "Message":"Login success"

}

puede utilizar ResponseBody (java POJO, ENUM, etc.) según sus necesidades.

Ved Prakash
fuente
2

Más correcto crear DTO para consultas de API, por ejemplo entityDTO:

  1. Respuesta predeterminada OK con lista de entidades:
@GetMapping(produces=MediaType.APPLICATION_JSON_VALUE)
@ResponseStatus(HttpStatus.OK)
public List<EntityDto> getAll() {
    return entityService.getAllEntities();
}

Pero si necesita devolver diferentes parámetros de Map, puede usar los dos ejemplos siguientes
2. Para devolver un parámetro como map:

@GetMapping(produces=MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Object> getOneParameterMap() {
    return ResponseEntity.status(HttpStatus.CREATED).body(
            Collections.singletonMap("key", "value"));
}
  1. Y si necesita un mapa de retorno de algunos parámetros (desde Java 9):
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Object> getSomeParameters() {
    return ResponseEntity.status(HttpStatus.OK).body(Map.of(
            "key-1", "value-1",
            "key-2", "value-2",
            "key-3", "value-3"));
}
Alexander Yushko
fuente
1

Si necesita devolver un objeto JSON usando una cadena, lo siguiente debería funcionar:

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.ResponseEntity;
...

@RestController
@RequestMapping("/student")
public class StudentController {

    @GetMapping
    @RequestMapping("/")
    public ResponseEntity<JsonNode> get() throws JsonProcessingException {
        ObjectMapper mapper = new ObjectMapper();
        JsonNode json = mapper.readTree("{\"id\": \"132\", \"name\": \"Alice\"}");
        return ResponseEntity.ok(json);
    }
    ...
}
Ashwin
fuente