← Volver al blog
Giancarlos Gaona·Publicado el 17 de marzo de 2026 · Actualizado el 3 de agosto de 2026·11 min de lectura

Headless Delivery vs JSONWS: elegir la API correcta en Liferay DXP

LiferayAPIJava

Dos APIs, una plataforma, mucha confusión

Si trabajas con Liferay DXP, en algún momento te habrás encontrado con una situación confusa: para obtener los mismos datos -- digamos, una lista de artículos web -- tienes al menos dos formas completamente diferentes de hacerlo. Una usa /api/jsonws y parece llamar directamente a métodos Java. La otra usa /o/headless-delivery/v1.0 y se comporta como una API REST moderna. Ambas funcionan, ambas devuelven datos, pero tienen filosofías radicalmente distintas.

Entender las diferencias entre JSONWS y Headless Delivery no es un ejercicio académico. La elección impacta la seguridad, mantenibilidad y futuro de tu proyecto. Después de trabajar con ambas APIs desde Liferay 7.1 hasta 7.4, puedo decir que la decisión no siempre es obvia, especialmente cuando migras proyectos legacy.

JSONWS: la API 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 automáticamente en un endpoint invocable vía HTTP.

Cómo funciona

Accede a /api/jsonws en cualquier instancia de Liferay y verás un explorador interactivo con cientos de servicios disponibles. Cada uno corresponde a un método 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 método JournalArticleService.getArticle() en el backend Java. La respuesta es una serialización directa del objeto Java, lo que significa que obtienes todos los campos internos de la entidad, incluidos muchos que probablemente no necesitas.

Ventajas de JSONWS

  • Acceso directo a todo: Cada servicio registrado en Liferay está disponible. Si existe un método Java para hacerlo, puedes invocarlo.
  • Simplicidad conceptual: Llamas a un método, pasas parámetros, obtienes resultado. No hay que pensar en recursos REST ni rutas semánticas.
  • Explorador integrado: La interfaz en /api/jsonws permite probar servicios directamente, ver parámetros y respuestas.

Problemas de JSONWS

  • Seguridad deficiente por defecto: JSONWS expone la superficie completa de servicios de Liferay. Por defecto, un usuario autenticado puede llamar a cualquier servicio para el que tenga permisos, incluyendo algunos potencialmente peligrosos. Sí existen mecanismos para acotarlo: jsonws.web.service.paths.includes y jsonws.web.service.paths.excludes en portal properties, y sobre todo las Service Access Policies, que declaran una whitelist por clase y método (DLAppService#get*) y que incluso se exponen como scopes OAuth2. La diferencia con Headless no es que JSONWS carezca de control de alcance, sino que ese control vive en configuración de plataforma que hay que activar a conciencia: por defecto, una petición autenticada con usuario y contraseña cae en la política SYSTEM_USER_PASSWORD, que permite invocar cualquier método.
  • Sin estándar: La respuesta es una serialización ad-hoc de objetos Java. No sigue OpenAPI, JSON:API, ni ningún estándar. Cada servicio tiene su propio formato de respuesta.
  • Acoplamiento fuerte: Tu código frontend depende de la firma exacta del método Java. Si Liferay cambia un parámetro o renombra un servicio entre versiones, tu integración se rompe.
  • CSRF acoplado a la sesión: si llamas desde el navegador con la cookie de sesión, cada petición necesita el token p_auth. Desde un cliente que no es navegador puedes usar HTTP Basic y saltarte p_auth, pero entonces estás mandando credenciales en cada llamada en lugar de un token revocable.
  • Sin paginación estándar: Algunos servicios aceptan start y end para paginar, otros no. No hay un formato uniforme de respuesta paginada.

Headless Delivery: la API moderna

Liferay introdujo su framework Headless, basado en la especificación OpenAPI, con Liferay Portal 7.2 GA1 (junio de 2019); los módulos se retroportaron después a 7.1 a partir del FixPack 12. Estas APIs están agrupadas bajo el prefijo /o/ y siguen convenciones REST estándar.

Endpoints principales

Liferay organiza sus APIs Headless en varios módulos:

  • /o/headless-delivery/v1.0: Contenido estructurado, documentos, blogs, páginas de sitio, Knowledge Base, foros, wiki y menús de navegación
  • /o/headless-admin-user/v1.0: Usuarios, organizaciones, roles
  • /o/headless-admin-taxonomy/v1.0: Vocabularios y categorías
  • /o/headless-commerce-delivery-catalog/v1.0: Productos y catálogo (Commerce)
  • /o/c/{objectName}: Objects personalizados (auto-generados)

Cómo funciona

# 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 estándar con paginación:

{
  "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 está estructurada, los campos tienen nombres semánticos, la paginación es consistente, y los datos están normalizados. No estás viendo la serialización cruda de un objeto Java.

Comparación directa

| Aspecto | JSONWS | Headless Delivery | |---------|--------|-------------------| | Base path | /api/jsonws | /o/headless-*/v1.0, /o/c/ | | Autenticación | Cookie + p_auth (navegador) o HTTP Basic | OAuth2, Basic Auth, Cookie | | Formato respuesta | Serialización Java ad-hoc | JSON estándar con schema OpenAPI | | Paginación | Inconsistente (start/end) | Estándar (page/pageSize/totalCount) | | Filtrado | Parámetros por método | OData syntax uniforme | | Documentación | Explorador en /api/jsonws | OpenAPI spec + /o/api | | Versionado | Sin versiones (cambia con Liferay) | Versionado en URL (v1.0, v2.0) | | Control de alcance | Whitelist global: jsonws.web.service.paths.includes/excludes y Service Access Policies (por clase y método) | Lo anterior más scopes OAuth2 asignados por aplicación | | GraphQL | No | Sí, en /o/graphql | | Futuro | Soportado, pero ya no recomendado | Modelo recomendado |

Por qué Liferay ya no recomienda JSONWS

Liferay ha sido gradual pero consistente en mover el ecosistema hacia Headless. Las razones principales son:

Seguridad

JSONWS expone internamente servicios que no estaban diseñados para acceso HTTP. La distinción entre *Service (con verificación de permisos) y *LocalService (sin verificación) es crítica en Java, y aunque JSONWS solo publica la capa *Service, nada garantiza que esa capa verifique nada: el método remoto lo escribe el desarrollador, y si delega en el LocalService sin comprobar permisos, el agujero queda expuesto por HTTP. Un desarrollador que no entiende esta distinción puede crear vulnerabilidades serias.

En Liferay DXP 7.4 JSONWS sigue habilitado por defecto (json.web.service.enabled=true) y el explorador de /api/jsonws sigue siendo visible; lo único excluido de fábrica es /user/update-password. Lo que ha cambiado no es el interruptor por defecto, sino la recomendación: Liferay documenta JSONWS como un framework antiguo, todavía soportado pero ya no recomendado, y deja en manos del administrador restringirlo con jsonws.web.service.paths.includes/excludes y Service Access Policies.

La cuestión de LocalService

Este es un punto que merece atención 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 módulos del servidor.
  • *Service: Wrapper que primero verifica que el usuario tiene permisos, y luego delega al LocalService.

En código Java dentro de Liferay, un módulo puede llamar a JournalArticleLocalService.getArticle() directamente, bypaseando la verificación de permisos. Esto es válido en ciertos contextos (un servicio del sistema que necesita acceso incondicional), pero peligroso si se expone externamente.

JSONWS nunca expuso los LocalServices: Service Builder solo anota con @JSONWebService la interfaz *Service de las entidades con remote-service="true". El riesgo real es otro, y sigue vigente: nada impide que un desarrollador anote a mano un *ServiceImpl que no verifica permisos, o que escriba un método remoto que delegue en el LocalService sin comprobar nada. Esa confusión persiste en proyectos que arrastran ese patrón.

Headless Delivery reduce mucho el riesgo: los endpoints que vienen de fábrica se apoyan en la capa *Service y respetan permisos, e incluso exponen subrecursos /permissions para gestionarlos. Lo que no desaparece es la responsabilidad del desarrollador: una API propia hecha con REST Builder no verifica permisos por sí sola, hay que delegar en los servicios remotos o comprobarlos explícitamente en el *ResourceImpl.

Estándares y ecosistema

JSONWS no sigue ningún estándar HTTP o API reconocido. Esto significa que herramientas estándar (Postman collections generadas automáticamente, generadores de clientes, documentación interactiva) no funcionan bien con JSONWS.

En /o/api tienes el API Explorer, y cada aplicación publica su especificación legible por máquina en /o/<schema>/v1.0/openapi.json (por ejemplo /o/headless-delivery/v1.0/openapi.json); desde 7.4 U69/GA69 también puedes descargar el openapi.json agregado de todo el portal. Puedes importar esa especificación en cualquier herramienta que soporte OpenAPI y generar automáticamente clientes en cualquier lenguaje, colecciones de Postman, o documentación.

La alternativa GraphQL

Además 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 útil cuando:

  • Necesitas datos de múltiples entidades en una sola petición
  • Quieres minimizar el payload (no recibes campos que no usas)
  • Tu frontend ya usa Apollo Client o similar

Sin embargo, en la práctica, REST sigue siendo la opción más común en proyectos Liferay porque la documentación y ejemplos están más orientados a REST, y la mayoría de equipos enterprise ya tienen herramientas establecidas para APIs REST.

Configuración de OAuth2

Para consumir Headless APIs desde fuera de Liferay (aplicaciones externas, Client Extensions remotas, integraciones), necesitas configurar OAuth2.

Client Credentials (para servicios backend)

Este flujo es el indicado para integraciones servidor-a-servidor donde no hay un usuario humano involucrado:

  1. En el Panel de Control de Liferay, ve a OAuth2 Administration
  2. Crea una nueva aplicación con:
    • Client Profile: Headless Server
    • Allowed Grant Types: Client Credentials
  3. Anota el Client ID y Client Secret
  4. Asigna scopes: selecciona qué APIs puede consumir esta aplicación

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.

Authorization Code (para aplicaciones frontend)

Para aplicaciones donde un usuario se autentica:

  1. Crea la aplicación OAuth2 con Allowed Grant Types: Authorization Code + PKCE
  2. Configura las Callback URIs de tu aplicación
  3. Tu aplicación redirige al usuario a Liferay para autenticarse
  4. Liferay redirige de vuelta con un código de autorización
  5. Tu backend intercambia el código por un token

Migración práctica: de JSONWS a Headless

Si tienes un proyecto que usa JSONWS y necesitas migrar, el enfoque gradual funciona mejor que una reescritura total.

Paso 1: Inventario

Lista todas las llamadas JSONWS en tu código. Busca Liferay.Service(, /api/jsonws, y p_auth como indicadores. Clasifica cada llamada por entidad (journal, user, document, etc.).

Paso 2: Mapeo de equivalencias

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/sites/{siteId}/taxonomy-categories
           (o /taxonomy-vocabularies/{vocabularyId}/taxonomy-categories)

Paso 3: Migración incremental

Reemplaza las llamadas una por una, empezando por las más usadas. Para cada una:

  1. Implementa la nueva llamada usando Headless
  2. Verifica que los datos devueltos contengan lo que tu frontend necesita (los formatos son diferentes)
  3. Adapta el parsing de la respuesta (la estructura JSON cambia)
  4. Prueba con los mismos permisos de usuario

Paso 4: Adaptar la autenticación

Si antes dependías de la cookie de sesión + CSRF token, evalúa si necesitas migrar a OAuth2. Para Client Extensions que corren dentro de Liferay, Liferay.Util.fetch() sigue manejando la autenticación automáticamente. Para integraciones externas, configura OAuth2.

Conclusión práctica

Mi recomendación después de varios proyectos con ambas APIs: si empiezas un proyecto nuevo en Liferay 7.4, usa exclusivamente Headless Delivery. No hay razón válida para elegir JSONWS en un proyecto greenfield.

Si mantienes un proyecto legacy, migra gradualmente. JSONWS sigue funcionando y sigue habilitado por defecto, pero Liferay ya lo documenta como un framework antiguo y no recomendado, y toda la inversión en producto va a Headless. Es mejor migrar proactivamente que quedarse anclado a una API que ya no recibe funcionalidad nueva.

Y si necesitas acceso a un servicio interno que Headless no expone, la solución correcta no es volver a JSONWS: es crear un endpoint Headless personalizado a través 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 dirección reduce la deuda técnica futura.

GG
Giancarlos Gaona

Ingeniero de Software y Consultor Senior especializado en Liferay DXP, con más de 8 años de experiencia liderando proyectos enterprise para banca, retail, energía e instituciones públicas.