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
| Feature | JSONP | CORS |
|---|---|---|
| HTTP methods | GET only | All methods |
| Browser support | All (including IE6) | IE10+ and all modern browsers |
| Error handling | Very limited | Full HTTP status codes |
| Security risk | XSS via callback injection | Safe when configured correctly |
| Recommended? | Legacy use only | Yes — preferred for all new APIs |
See Also
- Secure Your Jersey REST APIs
- How to Use Jersey Client to Call REST Services
- Handling Exceptions in JAX-RS Jersey with ExceptionMapper
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.