Jersey JAX-RS JSONP Example: Cross-Origin Requests with Callback Support

JSONP (JSON with Padding) is a legacy cross-origin communication technique that wraps a JSON payload inside a JavaScript function call, allowing a browser to load data from a different domain by exploiting the fact that <script> tags are not subject to the same-origin policy. While CORS is the modern standard, JSONP support is still requested when integrating with older browsers or APIs that do not expose CORS headers. Jersey provides first-class JSONP support through its built-in padding mechanism. This guide shows you exactly how to enable and use it.

How JSONP Works

A normal JSON response looks like:

{"id": 1, "name": "Alice", "email": "[email protected]"}

A JSONP response wraps the same JSON inside a callback function name supplied by the client:

myCallback({"id": 1, "name": "Alice", "email": "[email protected]"});

The client loads this as a <script src="https://api.example.com/users/1?callback=myCallback"> tag. When the script executes, myCallback is called with the data.

Maven Dependencies

<dependency>
    <groupId>org.glassfish.jersey.containers</groupId>
    <artifactId>jersey-container-servlet</artifactId>
    <version>3.1.5</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-json-jackson</artifactId>
    <version>3.1.5</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.inject</groupId>
    <artifactId>jersey-hk2</artifactId>
    <version>3.1.5</version>
</dependency>

Approach 1 — Using @JSONP Annotation (Jersey Built-in)

Jersey’s @JSONP annotation automatically wraps the JSON response with the callback name from a configurable query parameter:

package com.ankurm.resource;

import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import org.glassfish.jersey.server.JSONP;

@Path("/users")
public class UserResource {

    // Sample data model
    public record User(int id, String name, String email) {}

    /**
     * Regular JSON endpoint: GET /users/1
     * JSONP endpoint:        GET /users/1?callback=myFunction
     *
     * @JSONP tells Jersey: if a 'callback' query param is present,
     * wrap the JSON body with that function name.
     */
    @GET
    @Path("/{id}")
    @JSONP(queryParam = "callback")          // default queryParam is already "callback"
    @Produces({MediaType.APPLICATION_JSON,
               "application/javascript"})    // must include JS MIME for JSONP clients
    public User getUser(@PathParam("id") int id) {
        // In a real app, fetch from a database
        return new User(id, "Alice", "[email protected]");
    }

    /**
     * List endpoint with JSONP support.
     * Custom callback param name: 'cb'
     * Custom padding function: 'dataReady'
     */
    @GET
    @JSONP(queryParam = "cb", callback = "dataReady")
    @Produces({MediaType.APPLICATION_JSON, "application/javascript"})
    public java.util.List<User> listUsers() {
        return java.util.List.of(
            new User(1, "Alice", "[email protected]"),
            new User(2, "Bob",   "[email protected]")
        );
    }
}

Application Configuration

import org.glassfish.jersey.jackson.JacksonFeature;
import org.glassfish.jersey.server.ResourceConfig;
import jakarta.ws.rs.ApplicationPath;

@ApplicationPath("/api")
public class AppConfig extends ResourceConfig {
    public AppConfig() {
        packages("com.ankurm.resource");
        register(JacksonFeature.class);     // JSON serialisation
    }
}

Approach 2 — Manual JSONP Wrapper (Framework-agnostic)

When you need full control over the wrapper or when @JSONP is not available, build the response string manually:

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.Response;

@Path("/products")
public class ProductResource {

    private static final ObjectMapper MAPPER = new ObjectMapper();

    public record Product(int id, String name, double price) {}

    @GET
    @Path("/{id}")
    @Produces("application/javascript")
    public Response getProduct(
            @PathParam("id") int id,
            @QueryParam("callback") String callback) throws Exception {

        Product product = new Product(id, "Laptop", 999.99);
        String  json    = MAPPER.writeValueAsString(product);

        if (callback != null && callback.matches("[a-zA-Z_$][a-zA-Z0-9_$]*")) {
            // Validate callback name to prevent XSS injection
            String jsonp = callback + "(" + json + ");";
            return Response.ok(jsonp).build();
        }

        // Fall back to plain JSON if no callback supplied
        return Response.ok(json).type("application/json").build();
    }
}

Testing with curl and a Browser

# Regular JSON request
curl http://localhost:8080/api/users/1
# {"id":1,"name":"Alice","email":"[email protected]"}

# JSONP request with callback
curl "http://localhost:8080/api/users/1?callback=myFunction"
# myFunction({"id":1,"name":"Alice","email":"[email protected]"});

# List with custom cb param
curl "http://localhost:8080/api/users?cb=showUsers"
# dataReady([{"id":1,...},{"id":2,...}]);

In a browser, load the JSONP response as a script tag:

<pre class="wp-block-syntaxhighlighter-code"><script>
  function myFunction(data) {
    console.log('User:', data.name, '/', data.email);
  }
</script>
<a href="http://localhost:8080/api/users/1?callback=myFunction">http://localhost:8080/api/users/1?callback=myFunction</a></pre>

Security Considerations

  • Validate the callback name — always match against [a-zA-Z_$][a-zA-Z0-9_$.]* before echoing it in the response. An unsanitised callback allows XSS attacks.
  • Sensitive data — JSONP responses are globally readable by any page that includes the script tag. Never expose authentication tokens, personal data, or anything that should be access-controlled via JSONP.
  • Prefer CORS — for all new development use proper CORS headers (Access-Control-Allow-Origin) instead of JSONP. JSONP only supports GET requests and carries XSS risk.

JSONP vs CORS Quick Comparison

FeatureJSONPCORS
HTTP methodsGET onlyAll methods
Browser supportAll (including IE6)IE10+ and all modern browsers
Error handlingVery limitedFull HTTP status codes
Security riskXSS via callback injectionSafe when configured correctly
Recommended?Legacy use onlyYes — preferred for all new APIs

See Also

Conclusion

Jersey’s @JSONP annotation makes adding JSONP support to an existing endpoint a one-line change. For maximum control, the manual wrapper approach gives you full flexibility over the response format and callback validation. In either case, always sanitise the callback parameter name to prevent XSS, and prefer CORS for all new APIs — JSONP should only appear in codebases that need to support genuinely ancient browsers or third-party scripts that cannot be updated.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.