No serializer found for class in custom Java AWS application

Viewed 2922

I am creating a Spring application where in it calls information from the AWS API and converts it to JSON for a front end application to digest and display. I am currently trying to return a list out all of the current S3 buckets on an account but am running into an issue where the JSON response is:

status": 500, "error": "Internal Server Error", "message": "Type definition error: [simple type, class software.amazon.awssdk.services.s3.model.Bucket]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class software.amazon.awssdk.services.s3.model.Bucket and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: com.landsend.clouddashboard.data.Class.S3[\"bucketList\"]-java.util.Collections$UnmodifiableRandomAccessList[0])", "path": "/api/s3/buckets"

I have created a class called S3 that has private variables. One of them being a List type with the name bucketList. This class has the representative getters and setters as well associated with those variables.

I have also tried adding this to the applications.properties file:

spring.jackson.serialization.FAIL_ON_EMPTY_BEANS=false

This did result in the error disappearing but I ended up with an empty JSON file.

Controller Class

@RestController
@RequestMapping(value="/api")
public class S3Controller {

    @Autowired
    private S3Service s3Service;

    @RequestMapping(method= RequestMethod.GET, value="/s3/buckets")
    public S3 ListBuckets(){
        return s3Service.listBucket();
    }
}

Service Class

@Service
public class S3Service {

    private S3Access s3Access;

    @Autowired
    public S3Service(S3Access s3Access){
        this.s3Access = s3Access;
    }
    public S3 listBucket(){
        //Any Additional business logic would go here
        return s3Access.listBucket();
    }
}

Access Class

@Repository
public class S3Access implements S3Repository {

    @Autowired
    private S3 s3Instance;

    private Region region = Region.US_EAST_1;
    private S3Client s3Client = 
                     S3Client.builder().region(region).build();

    @Override
    public S3 listBucket() {
        ListBucketsRequest listBucketsRequest = ListBucketsRequest.builder().build();
        ListBucketsResponse listBucketsResponse = s3Client.listBuckets(listBucketsRequest);
        s3Instance.setBucketList(listBucketsResponse.buckets());
        return s3Instance;
    }
}

S3 Constructor Class

@Component
public class S3 {
    private String fileName;
    private String bucketName;
    private int bucketSize;
    private List<Bucket> bucketList;

    public List<Bucket> getBucketList() {
        return bucketList;
    }

    public void setBucketList(List<Bucket> bucketList) {
        this.bucketList = bucketList;
    }
}

Any thoughts on why this isn't working is appreciated.

3 Answers

Problem is with Bucket class. Jackson by default treats all provided objects like POJO-s. Each POJO should have list of getters/setters. In case, class does not have them object is treated as empty. If you want to serialise classes like this you can:

  1. Create extra POJO class with getters and map it from raw AWS class to this POJO.
  2. Implement and register serialiser for this class.
  3. Convert AWS model to Map-s and List-s manually and serialise them.

See also:

In addition to the accepted answer above, another method of solving this problem is to extract the parts of the list and insert them as their separated, respective lists into the prebuilt POJO.

In this example, I took the name() and creationDate() functions and put them in their own list in the S3 Class. This allows Jackson to read the S3 Class and get the required information without having to build an extra POJO for the Bucket class. Not entirely sure which way is actually cleaner or more effective but this also worked for what I was doing.

S3 Construction Class

@Component
public class S3 {
    private ArrayList<String> bucketList;
    private ArrayList<Instant> bucketCreationDate;

    public ArrayList<Instant> getBucketCreationDate() {
        return bucketCreationDate;
    }

    public void setBucketCreationDate(ArrayList<Instant> bucketCreationDate) {
        this.bucketCreationDate = bucketCreationDate;
    }

    public ArrayList<String> getBucketList() {
        return bucketList;
    }

    public void setBucketList(ArrayList<String> bucketList) {
        this.bucketList = bucketList;
    }
}

Access Class

@Repository
public class S3Access implements S3Repository {

    @Autowired
    private S3 s3Instance;

    private Region region = Region.US_EAST_1;
    private S3Client s3Client = S3Client.builder().region(region).build();
    private ArrayList <String> bucketNameList = new ArrayList<>();
    private ArrayList <Instant> bucketCreationDate = new ArrayList<>();

    @Override
    public S3 listBucket() {
        ListBucketsRequest listBucketsRequest = ListBucketsRequest.builder().build();
        ListBucketsResponse listBucketsResponse = s3Client.listBuckets(listBucketsRequest);

        // Adding Bucket Names to Array List
        for (int i = 0; i < listBucketsResponse.buckets().size() ; i++) {
            bucketNameList.add(listBucketsResponse.buckets().get(i).name());
        }

        // Adding Bucket Creation Dates to Array List
        for (int i = 0; i < listBucketsResponse.buckets().size() ; i++) {
            bucketCreationDate.add(listBucketsResponse.buckets().get(i).creationDate());
        }

        // Setting the lists in the S3 Class
        s3Instance.setBucketCreationDate(bucketCreationDate);
        s3Instance.setBucketList(bucketNameList);

        return s3Instance;


apparently the builder class is serializable (since they have public getters).I have tried it on the findings returned by getFindigs API for GuardDuty in a restController class and it did serialize the object successfully. so just call toBuilder() method on the returned pojo objects and return that instead of copying stuff over .

    List<Finding.Builder> findingResult = findings.stream().map(finding -> finding.toBuilder())
                    .collect(Collectors.toList());
    return findingResult;
Related