Skip to content

@ExposeField -> Search

Latest

Choose a tag to compare

@notablogger notablogger released this 26 Oct 13:42
ac09862

spring-xpose 4.0.0

Highlights

Search — POST /api/<path>/search

Add Operation.SEARCH to expose and declare which filters each field accepts:

@Entity
@ExposeEntity(path = "products", pageable = true,
              expose = {Operation.FIND_ALL, Operation.FIND_BY_ID, Operation.CREATE,
                        Operation.UPDATE, Operation.DELETE, Operation.SEARCH})
public class Product {
    @ExposeField(filterOperations = {FilterOperation.EQ, FilterOperation.LIKE})
    private String name;

    @ExposeField(filterOperations = {FilterOperation.GTE, FilterOperation.LTE})
    private BigDecimal price;

    @ManyToOne
    @ExposeField(filterOperations = {FilterOperation.EQ, FilterOperation.IN})
    private Category category;

    @ElementCollection
    @ExposeField(filterOperations = {FilterOperation.IN, FilterOperation.NOT_IN, FilterOperation.IS_NULL})
    private List<String> tags;
}
curl -X POST "http://localhost:8080/api/products/search?page=0&size=20&sort=price,desc" \
  -H "Content-Type: application/json" \
  -d '{"name_like": "laptop", "price_lte": 2000, "categoryId_in": [1, 2], "tags_in": ["sale"]}'
  • Operations: EQ, NE, GT, GTE, LT, LTE, LIKE (case-insensitive contains), IN, NOT_IN, IS_NULL. EQ uses the field name as its key; every other operation uses <field>_<operation> (price_gte, status_not_in, deletedAt_is_null).
  • Every key is optional and keys are combined with AND; {} returns every record.
  • Collections (@ElementCollection, MongoDB arrays): IN / NOT_IN mean has any / has none of the values; IS_NULL means null or empty.
  • Relations filter by the related id: "categoryId": 5, "suppliers_in": [1, 2].
  • Paging and sorting with pageable = true (page, size, sort query parameters).
  • A read: secured with readRoles, not writeRoles; safe to retry.
  • Strict: an unknown key — a typo, an operation the field doesn't declare, a hidden field — is a 400 that lists the allowed keys; values are type-checked (400 keyed by the JSON key).
  • Checked at compile time: e.g. LIKE on a number, ranges on a non-comparable type, or filters on a hidden field fail the build.
  • Swagger UI: a typed request-body schema and a ready-to-send example.
  • JPA and MongoDB.

Why POST: a GET cannot reliably carry a request body, and a body keeps filters typed and free of URL length limits. Serving search over the HTTP QUERY method (RFC 10008) is planned once Spring supports it, with the same body.

→ Search reference

Hide fields with @ExposeField(hidden = true)

@ExposeField(hidden = true)
private String passwordHash;   // stored, never returned or accepted by the API

Left out of both the response and the request DTO; works on scalar fields, relations and MongoDB fields.

Clearer 400 for unreadable request bodies

The urn:springxpose:malformed-body problem now names the failing property — detail says where, and errors is keyed by the JSON path:

{ "type": "urn:springxpose:malformed-body", "status": 400,
  "detail": "The request body is invalid at 'price': Cannot deserialize value of type `java.math.BigDecimal` from String \"abc\": not a valid representation",
  "errors": { "price": "Cannot deserialize value of type `java.math.BigDecimal` from String \"abc\": not a valid representation" } }

Paging documented correctly in OpenAPI

With pageable = true, Swagger UI shows page, size and sort as optional query parameters.

Breaking changes

  • ignoredFields is removed from @ExposeEntity and @ExposeDocument. Mark each field instead:

    // 3.x
    @ExposeEntity(path = "users", ignoredFields = {"passwordHash"})
    public class User { private String passwordHash; }
    
    // 4.0
    @ExposeEntity(path = "users")
    public class User { @ExposeField(hidden = true) private String passwordHash; }
  • Malformed-body 400 responses gained an errors property and a different detail text (see above). Clients that parse detail should switch to errors.

Fixes

  • Entities with a generic or array field (List<String>, Set<Integer>, byte[]) now generate their DTOs.

Upgrading

  1. Bump spring-xpose-starter and spring-xpose-processor to 4.0.0.
  2. Replace every ignoredFields = {...} with @ExposeField(hidden = true) on those fields.
  3. Optional: add Operation.SEARCH and @ExposeField(filterOperations = ...) where clients need filtering.

Sample project: spring-xpose-sample-rest