javajacksonmicronautmicronaut-datamicronaut-serde

Micronaut's Page used as a controller method return type is serialized without Page properties


I'm trying to implement a simple paged API using Micronaut 3.7.9, with micronaut-serde-jackson and I've encountered an issue with how my response is serialized, as Page properties are not included. My controller method return type is io.micronaut.data.model.Page.

Instead of getting a response looking like this (example response generated with micronaut-openapi's swagger):

{
  "content": [
  {
    "id": 0,
    "name": "Group1"
  }
  ],
  "pageable": {
    "orderBy": [
      {
        "ignoreCase": true,
        "direction": "ASC",
        "property": "string",
        "ascending": true
      }
    ],
    "number": 0,
    "size": 0,
    "sort": {
      "orderBy": [
        {
          "ignoreCase": true,
          "direction": "ASC",
          "property": "string",
          "ascending": true
        }
      ]
    },
    "sorted": true
  },
  "pageNumber": 0,
  "offset": 0,
  "size": 0,
  "empty": true,
  "numberOfElements": 0,
  "totalSize": 0,
  "totalPages": 0
}

I'm getting just the plain array of the items from pageable's content array:

[
  {
    "id": 0,
    "name": "Group1"
  }
]

I've traced this issue back to the io.micronaut.serde.support.DefaultSerdeRegistry's findSerializer method which returns a io.micronaut.serde.support.serializers.IterableSerializer for a Page instance (which is understandable as Page effectively implements Iterable).

I'm wondering if there is a way to configure Serde to serialize Pageable with its instance fields (size, totalPages etc.), other than creating a custom Page which does not inherit from Iterable or registering own Serializer instance especially for io.micronaut.data.model.Page?

[EDIT] Relevant classes:

Page:

https://github.com/micronaut-projects/micronaut-data/blob/master/data-model/src/main/java/io/micronaut/data/model/Page.java

Repository:

package org.example.repository;

import io.micronaut.data.annotation.Repository;
import io.micronaut.data.repository.PageableRepository;
import org.example.model.Group;

@Repository
public interface GroupRepository extends PageableRepository<Group, Long> {
}

Service:

package org.example.service;

import io.micronaut.data.model.Page;
import io.micronaut.data.model.Pageable;
import jakarta.inject.Singleton;
import lombok.RequiredArgsConstructor;
import org.example.dto.GroupDto;
import org.example.repository.GroupRepository;

@Singleton
@RequiredArgsConstructor
public class GroupService {
    private final GroupRepository groupRepository;

    public Page<GroupDto> findAllGroups(Pageable pageable) {
        return groupRepository.findAll(pageable).map(GroupDto::from);
    }
}

Controller:

package org.example.controller;

import io.micronaut.data.model.Page;
import io.micronaut.data.model.Pageable;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.swagger.v3.oas.annotations.Parameter;
import lombok.RequiredArgsConstructor;
import org.example.dto.GroupDto;
import org.example.service.GroupService;

@Controller("/groups")
@RequiredArgsConstructor
public class GroupController {
    private final GroupService groupService;

    @Get
    public Page<GroupDto> findAllGroups(@Parameter(hidden = true) Pageable pageable) {
        return groupService.findAllGroups(pageable);
    }
}

DTO

package org.example.dto;

import io.micronaut.serde.annotation.Serdeable;
import org.example.model.Group;

@Serdeable
public record GroupDto(Long id, String name, int version) {
    public static GroupDto from(Group group) {
        return new GroupDto(group.getId(), group.getName(), group.getVersion());
    }
}

Entity:

package org.example.model;

import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;

@Entity
@Table(name = "_group")
@NoArgsConstructor @Getter @Setter
public class Group {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
    private String name;
    @Version
    private int version;
}

Solution

  • You can use a custom serializer:

    @Prototype
    final class DefaultPageSerializer implements Serializer<DefaultPage<Object>> {
    
        @Override
        public void serialize(Encoder encoder, EncoderContext context, Argument<? extends DefaultPage<Object>> type, DefaultPage<Object> page) throws IOException {
            Encoder e = encoder.encodeObject(type);
    
            e.encodeKey("content");
            Argument<List<Object>> contentType = Argument.listOf((Argument<Object>) type.getFirstTypeVariable().orElse(Argument.OBJECT_ARGUMENT));
            context.findSerializer(contentType)
                .createSpecific(context, contentType)
                .serialize(e, context, contentType, page.getContent());
    
            e.encodeKey("pageable");
            Argument<Pageable> pageable = Argument.of(Pageable.class);
            context.findSerializer(pageable)
                .createSpecific(context, pageable)
                .serialize(e, context, pageable, page.getPageable());
    
            e.encodeKey("totalSize");
            e.encodeLong(page.getTotalSize());
    
            e.finishStructure();
        }
    }
    

    This should be fixed in Micronaut 4.