Si trabajas con Liferay DXP, en algun momento te habras encontrado con una situacion confusa: para obtener los mismos datos -- digamos, una lista de articulos web -- tienes al menos dos formas completamente diferentes de hacerlo. Una usa /api/jsonws y parece llamar directamente a metodos Java. La otra usa /o/headless-delivery/v1.0 y se comporta como una API REST moderna. Ambas funcionan, ambas devuelven datos, pero tienen filosofias radicalmente distintas.
Entender las diferencias entre JSONWS y Headless Delivery no es un ejercicio academico. La eleccion impacta la seguridad, mantenibilidad y futuro de tu proyecto. Despues de trabajar con ambas APIs desde Liferay 7.1 hasta 7.4, puedo decir que la decision no siempre es obvia, especialmente cuando migras proyectos legacy.
JSONWS (JSON Web Services) fue el primer intento de Liferay de exponer sus servicios internos como APIs HTTP. Cada servicio Java registrado en Liferay se convierte automaticamente en un endpoint invocable via HTTP.
Accede a /api/jsonws en cualquier instancia de Liferay y veras un explorador interactivo con cientos de servicios disponibles. Cada uno corresponde a un metodo Java real. Por ejemplo:
# Obtener un articulo web por groupId y articleId
/api/jsonws/journal.journalarticle/get-article \
-d groupId=20123 \
-d articleId=MI-ARTICULO \
-d version=1.0
Desde JavaScript en el contexto de Liferay, usas el objeto global Liferay.Service():
Liferay.Service(
'/journal.journalarticle/get-article',
{
groupId: themeDisplay.getScopeGroupId(),
articleId: 'MI-ARTICULO',
version: 1.0,
},
function (article) {
console.log(article.title)
}
)
Esto ejecuta una llamada AJAX que invoca directamente el metodo JournalArticleService.getArticle() en el backend Java. La respuesta es una serializacion directa del objeto Java, lo que significa que obtienes todos los campos internos de la entidad, incluidos muchos que probablemente no necesitas.
/api/jsonws permite probar servicios directamente, ver parametros y respuestas.p_auth para proteccion CSRF, lo que complica las llamadas desde fuera del contexto de Liferay.start y end para paginar, otros no. No hay un formato uniforme de respuesta paginada.A partir de Liferay 7.1 (y mejorando significativamente en 7.2+), Liferay introdujo APIs basadas en la especificacion OpenAPI. Estas APIs estan agrupadas bajo el prefijo /o/ y siguen convenciones REST estandar.
Liferay organiza sus APIs Headless en varios modulos:
/o/headless-delivery/v1.0: Contenido estructurado, documentos, blogs, categorias, paginas/o/headless-admin-user/v1.0: Usuarios, organizaciones, roles/o/headless-admin-taxonomy/v1.0: Vocabularios y categorias/o/headless-commerce-delivery-catalog/v1.0: Productos y catalogo (Commerce)/o/c/{objectName}: Objects personalizados (auto-generados)# Obtener contenido estructurado de un site
curl -X GET \
"http://localhost:8080/o/headless-delivery/v1.0/sites/20123/structured-contents" \
-H "Authorization: Bearer eyJhbGciOiJSUzI1NiI..." \
-H "Accept: application/json"
La respuesta sigue un formato estandar con paginacion:
{
"items": [
{
"id": 45231,
"title": "Bienvenida",
"dateCreated": "2025-06-15T10:30:00Z",
"dateModified": "2025-06-20T14:22:00Z",
"contentFields": [
{
"name": "contenido",
"contentFieldValue": {
"data": "<p>Texto del articulo...</p>"
}
}
]
}
],
"page": 1,
"pageSize": 20,
"totalCount": 134,
"lastPage": 7
}
Nota la diferencia: la respuesta esta estructurada, los campos tienen nombres semanticos, la paginacion es consistente, y los datos estan normalizados. No estas viendo la serializacion cruda de un objeto Java.
| Aspecto | JSONWS | Headless Delivery |
|---------|--------|-------------------|
| Base path | /api/jsonws | /o/headless-*/v1.0, /o/c/ |
| Autenticacion | Cookie + p_auth (CSRF) | OAuth2, Basic Auth, Cookie |
| Formato respuesta | Serializacion Java ad-hoc | JSON estandar con schema OpenAPI |
| Paginacion | Inconsistente (start/end) | Estandar (page/pageSize/totalCount) |
| Filtrado | Parametros por metodo | OData syntax uniforme |
| Documentacion | Explorador en /api/jsonws | OpenAPI spec + /o/api |
| Versionado | Sin versiones (cambia con Liferay) | Versionado en URL (v1.0, v2.0) |
| Scopes | Todo o nada | OAuth2 scopes granulares |
| GraphQL | No | Si, en /o/graphql |
| Futuro | Deprecated progresivamente | Modelo recomendado |
Liferay ha sido gradual pero consistente en mover el ecosistema hacia Headless. Las razones principales son:
JSONWS expone internamente servicios que no estaban disenados para acceso HTTP. La distincion entre *Service (con verificacion de permisos) y *LocalService (sin verificacion) es critica en Java pero invisible via JSONWS. Un desarrollador que no entiende esta distincion puede crear vulnerabilidades serias.
En versiones recientes de Liferay, el acceso a JSONWS se ha restringido progresivamente. En Liferay DXP 7.4, muchos servicios estan deshabilitados por defecto y requieren configuracion explicita para habilitarse.
Este es un punto que merece atencion especial. En la arquitectura interna de Liferay, cada entidad tiene dos capas de servicio:
*LocalService: Ejecuta operaciones directamente, sin verificar permisos. Es para uso interno entre modulos del servidor.*Service: Wrapper que primero verifica que el usuario tiene permisos, y luego delega al LocalService.En codigo Java dentro de Liferay, un modulo puede llamar a JournalArticleLocalService.getArticle() directamente, bypaseando la verificacion de permisos. Esto es valido en ciertos contextos (un servicio del sistema que necesita acceso incondicional), pero peligroso si se expone externamente.
JSONWS originalmente exponia ambos. Liferay fue cerrando el acceso a LocalServices via HTTP progresivamente, pero la confusion persiste en proyectos que se iniciaron en versiones anteriores.
Headless Delivery resuelve esto de raiz: todos los endpoints pasan por la capa de permisos. No hay forma de bypasear la verificacion desde la API REST.
JSONWS no sigue ningun estandar HTTP o API reconocido. Esto significa que herramientas estandar (Postman collections generadas automaticamente, generadores de clientes, documentacion interactiva) no funcionan bien con JSONWS.
Headless Delivery expone un spec OpenAPI completo en /o/api. Puedes importar esta especificacion en cualquier herramienta que soporte OpenAPI y generar automaticamente clientes en cualquier lenguaje, colecciones de Postman, o documentacion.
Ademas de REST, Liferay expone un endpoint GraphQL en /o/graphql que permite consultar los mismos datos disponibles en Headless Delivery pero solicitando solo los campos que necesitas.
query {
structuredContents(siteKey: "20123", filter: "title eq 'Inicio'") {
items {
id
title
dateModified
contentFields {
name
contentFieldValue {
data
}
}
}
totalCount
}
}
GraphQL es particularmente util cuando:
Sin embargo, en la practica, REST sigue siendo la opcion mas comun en proyectos Liferay porque la documentacion y ejemplos estan mas orientados a REST, y la mayoria de equipos enterprise ya tienen herramientas establecidas para APIs REST.
Para consumir Headless APIs desde fuera de Liferay (aplicaciones externas, Client Extensions remotas, integraciones), necesitas configurar OAuth2.
Este flujo es el indicado para integraciones servidor-a-servidor donde no hay un usuario humano involucrado:
Para obtener un token:
curl -X POST "http://localhost:8080/o/oauth2/token" \
-d "grant_type=client_credentials" \
-d "client_id=tu-client-id" \
-d "client_secret=tu-client-secret"
La respuesta incluye un access_token que usas en el header Authorization: Bearer.
Para aplicaciones donde un usuario se autentica:
Si tienes un proyecto que usa JSONWS y necesitas migrar, el enfoque gradual funciona mejor que una reescritura total.
Lista todas las llamadas JSONWS en tu codigo. Busca Liferay.Service(, /api/jsonws, y p_auth como indicadores. Clasifica cada llamada por entidad (journal, user, document, etc.).
Para cada llamada JSONWS, encuentra el endpoint Headless equivalente. Ejemplos comunes:
# Articulos web
JSONWS: /api/jsonws/journal.journalarticle/get-articles
Headless: /o/headless-delivery/v1.0/sites/{siteId}/structured-contents
# Documentos
JSONWS: /api/jsonws/dlapp/get-file-entries
Headless: /o/headless-delivery/v1.0/sites/{siteId}/documents
# Usuarios
JSONWS: /api/jsonws/user/get-user-by-id
Headless: /o/headless-admin-user/v1.0/user-accounts/{id}
# Categorias
JSONWS: /api/jsonws/assetcategory/get-categories
Headless: /o/headless-admin-taxonomy/v1.0/taxonomy-categories
Reemplaza las llamadas una por una, empezando por las mas usadas. Para cada una:
Si antes dependias de la cookie de sesion + CSRF token, evalua si necesitas migrar a OAuth2. Para Client Extensions que corren dentro de Liferay, Liferay.Util.fetch() sigue manejando la autenticacion automaticamente. Para integraciones externas, configura OAuth2.
Mi recomendacion despues de varios proyectos con ambas APIs: si empiezas un proyecto nuevo en Liferay 7.4, usa exclusivamente Headless Delivery. No hay razon valida para elegir JSONWS en un proyecto greenfield.
Si mantienes un proyecto legacy, migra gradualmente. JSONWS sigue funcionando, pero cada version de Liferay restringe mas su acceso. Es mejor migrar proactivamente que verse forzado cuando una actualizacion rompa algo.
Y si necesitas acceso a un servicio interno que Headless no expone, la solucion correcta no es volver a JSONWS: es crear un endpoint Headless personalizado a traves de REST Builder o exponer la funcionalidad como un Object Action. El ecosistema de Liferay se mueve firmemente hacia Headless, y alinear tu proyecto con esa direccion reduce la deuda tecnica futura.