data-bindingballerina-swan-lakeballerina-http

Payload data binding doesn't work when the return is a union type


I'm trying to run the access the following service in my code.

Following code segment illustrates the service:

import ballerina/http;

type Album readonly & record {|
    string title;
    string artist;
|};

type IdNotFound record {|
    *http:NotFound;
    record {
        string message;
    } body;
|};

table<Album> key(title) albums = table [
    {title: "Blue Train", artist: "John Coltrane"},
    {title: "Jeru", artist: "Gerry Mulligan"}
];

service / on new http:Listener(8022) {

    resource function get album/[string id]() returns Album|IdNotFound {
        if (!albums.hasKey(id)) {
            return {body: {message: "No matching resource found."}};
        }
        return albums.get(id);
    }
}

Following is my code in main.bal file

import ballerina/http;
import ballerina/io;

type Album readonly & record {
    string title;
    string artist;
};

type IdNotFound record {|
    *http:NotFound;
    record {
        string message;
    } body;
|};

public function main() returns error? {
    // Creates a new client with the Basic REST service URL.
    http:Client albumClient = check new ("localhost:8022");

    string id = "Blue Train";
    Album|IdNotFound album = check albumClient->/album/[id];
    io:println("GET request:" + album.toJsonString());
}

When tried to run this code, I'm getting the following error in line Album|IdNotFound album = check albumClient->/album/[id];

line

incompatible type for parameter 'targetType' with inferred typedesc value: expected 'typedesc<(ballerina/http:2.8.0:Response|anydata)>', found 'typedesc<(wso2healthcare/abc:0.2.3:Album|wso2healthcare/abc:0.2.3:IdNotFound)>'(BCE3931)

Can I know what is wrong with my code?


Solution

  • You can only bind the response payload (which is expected to be a subtype of anydata). The error is because you are also using IdNotFound (which is not anydata) to try and bind to it.

    You can take a similar approach to the following.

    Album|http:ClientError album = albumClient->/album/[id];
    
    if album is Album {
        string title = album.title;
        io:println("Title: ", title);
        return;
    }
    
    if album !is http:ClientRequestError {
        return album;
    }
    
    // album is http:ClientRequestError here
    // 4xx errors
    record {
        string message;
    } body = check album.detail().body.ensureType();
    io:println("Message: ", body.message);
    

    If you have a requirement to wrap all errors using a spec-defined error message structure, you can use ResponseErrorInterceptor to change the error structure like this:

    type ErrorDetails record {|
        string timestamp;
        string message;
    |};
    
    // This can handle all the interceptable errors
    service class ErrorInterceptor {
        *http:ResponseErrorInterceptor;
    
        // We can access error and the already built response wrt to the error type
        remote function interceptResponseError(error err, http:Response errResponse) returns error {
            // Changing all the errors
            // Construct the spec defined error body
            ErrorDetails errDetails = {
                timestamp: time:utcToString(time:utcNow()),
                message: err.message()
            };
            // Return a default status code error with the error body
            // Set the cause to the original error to maintain the status code
            return error httpscerr:DefaultStatusCodeError("default error", err, body = errDetails);
        }
    }
    
    service http:InterceptableService / on new http:Listener(8022) {
        public function createInterceptors() returns ErrorInterceptor {
            return new ErrorInterceptor();
        }
    
        ...
    }