Skip to content

Commit 010b0c2

Browse files
committed
FEATURE: Unified JSON Projection API
- add documentation - fix #3853
1 parent b48a81f commit 010b0c2

File tree

3 files changed

+120
-1
lines changed

3 files changed

+120
-1
lines changed

docs/asciidoc/modules/modules.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ Modules are distributed as separate dependencies. Below is the catalog of offici
4040
* link:{uiVersion}/modules/vertx[Vertx]: Vertx module for Jooby.
4141

4242
==== JSON
43+
* link:{uiVersion}/modules/avaje-jsonb[Avaje-JsonB]: Avaje-JsonB module for Jooby.
4344
* link:{uiVersion}/modules/gson[Gson]: Gson module for Jooby.
4445
* link:{uiVersion}/modules/jackson2[Jackson2]: Jackson2 module for Jooby.
4546
* link:{uiVersion}/modules/jackson3[Jackson3]: Jackson3 module for Jooby.
4647
* link:{uiVersion}/modules/yasson[JSON-B]: JSON-B module for Jooby.
47-
* link:{uiVersion}/modules/avaje-jsonb[Avaje-JsonB]: Avaje-JsonB module for Jooby.
4848

4949
==== OpenAPI
5050
* link:{uiVersion}/modules/openapi[OpenAPI]: OpenAPI supports.

docs/asciidoc/mvc-api.adoc

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -577,6 +577,59 @@ If a request is made to `/bar?foo=baz`, the result will be `foo is: baz` because
577577

578578
==== Responses
579579

580+
===== Projections
581+
582+
The MVC module provides first-class support for Projections via annotations. This allows you to define the response view declaratively, keeping your controller logic clean and focused on data retrieval.
583+
584+
====== Usage
585+
586+
There are two ways to define a projection in an MVC controller.
587+
588+
You can annotate your method with `@Project` and provide the selection DSL:
589+
590+
.Via @Project Annotation
591+
[source,java]
592+
----
593+
@GET
594+
@Project("(id, name)")
595+
public List<User> listUsers() {
596+
return service.findUsers();
597+
}
598+
----
599+
600+
Alternatively, you can define the projection directly within the HTTP method annotation (e.g., `@GET`, `@POST`) using the `projection` attribute:
601+
602+
.Via HTTP Method Attribute
603+
[source,java]
604+
----
605+
@GET(value = "/users", projection = "(id, name, email)")
606+
public List<User> listUsers() {
607+
return service.findUsers();
608+
}
609+
----
610+
611+
====== Automatic Wrapping
612+
613+
The Jooby Annotation Processor automatically handles the conversion of your method's return type. You are **not forced** to return a `Projected` instance; you can simply return your POJO or Collection, and Jooby will wrap it for you at compile-time.
614+
615+
However, if you need manual control (for example, to dynamically toggle validation), you can still return a `Projected` instance explicitly:
616+
617+
[source,java]
618+
----
619+
@GET
620+
public Projected<User> getUser(String id) {
621+
User user = service.findById(id);
622+
return Projected.wrap(user)
623+
.failOnMissingProperty(true)
624+
.include("(id, status)");
625+
}
626+
----
627+
628+
[NOTE]
629+
====
630+
For more details on the Selection DSL syntax and available JSON engines, please refer to the <<core-responses-projections, Core Projections documentation>>.
631+
====
632+
580633
===== Status Code
581634

582635
The default HTTP status code returned by an MVC route is `200 OK`, except for `void` methods annotated with `@DELETE`, which automatically return `204 No Content`.

docs/asciidoc/responses.adoc

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,72 @@ Raw responses are **not** processed by a <<core-context-response-body-message-en
3737

3838
Even if a JSON encoder is installed, a raw response is always sent directly to the client bypassing the encoder.
3939

40+
==== Projections
41+
42+
Projections allow API consumers to request a partial representation of a resource. This feature is a more flexible, dynamic, and powerful alternative to the standard Jackson `@JsonView` annotation.
43+
44+
While inspired by the "selection set" philosophy of **GraphQL**, it is important to note that this is **not** a GraphQL implementation. Instead, it provides a "Light GraphQL" experience for RESTful endpoints, allowing you to define exactly which fields should be serialized in the JSON response without the overhead of a full GraphQL engine.
45+
46+
===== Basic Usage
47+
48+
To enable a projection, you wrap your response object using the `Projected.wrap` utility. The projection syntax is parsed and applied to the underlying object or collection.
49+
50+
[source,java]
51+
----
52+
get("/users/{id}", ctx -> {
53+
User user = repository.findById(ctx.path("id").value());
54+
55+
return Projected.wrap(user)
56+
.include("(id, name, email)");
57+
});
58+
----
59+
60+
===== Comparison with @JsonView
61+
62+
If you have used Jackson's `@JsonView`, you will find Projections far more capable:
63+
64+
* **Dynamic**: Unlike `@JsonView`, which requires static class markers defined at compile-time, Projections are defined at runtime.
65+
* **Ad-hoc**: You can create any combination of fields on the fly without adding new Java interfaces or classes.
66+
* **Deep Nesting**: Projections easily handle deeply nested object graphs, whereas `@JsonView` can become difficult to manage with complex relationships.
67+
68+
===== Projection DSL
69+
70+
The `include` method accepts a string using a simple, nested syntax:
71+
72+
* **Field Selection**: `(id, name)` returns only those two fields.
73+
* **Nested Selection**: `(id, address(city, country))` selects the `id` and specific fields from the nested `address` object.
74+
* **Wildcards**: `(id, address(*))` selects the `id` and all available fields within the `address` object.
75+
* **Deep Nesting**: `(id, orders(id, items(name, price)))` allows for recursion into the object graph.
76+
77+
===== Validation
78+
79+
By default, the projection engine **does not validate** that requested fields exist on the target class (`failOnMissingProperty` is `false`). This allows for maximum flexibility, especially when working with polymorphic types or dynamic data where certain fields may only exist on specific subclasses.
80+
81+
If you prefer strict enforcement to prevent API consumers from requesting non-existent fields, you can enable validation:
82+
83+
[source,java]
84+
----
85+
return Projected.wrap(data)
86+
.failOnMissingProperty(true)
87+
.include("(id, name, strictFieldOnly)");
88+
----
89+
90+
===== More Information
91+
92+
Support for Projections extends beyond core scripting to include high-level annotations and documentation generation.
93+
94+
* **MVC Support**: Projections can be applied to controller methods using the `@Project` annotation. See the <<web-mvc-api-responses-projections, MVC documentation>> for details.
95+
* **OpenAPI Support**: Jooby automatically generates pruned schemas for your Swagger documentation. See the link:modules/openapi[OpenAPI documentation] for details.
96+
97+
[NOTE]
98+
====
99+
**Implementation Note:**
100+
The `Projection` core API defines the structure and the DSL. The actual runtime filtering is performed by your chosen JSON module:
101+
102+
1. link:modules/avaje-jsonb[Avaje Jsonb]
103+
2. link:modules/jackson2[Jackson 2]/link:modules/jackson3[Jackson 3]
104+
====
105+
40106
==== Streaming / Chunked
41107

42108
The Streaming/Chunked API is available via:

0 commit comments

Comments
 (0)