DEV Community

Cover image for SpringBoot - Contextualizing serialization
GiBoOw
GiBoOw

Posted on

SpringBoot - Contextualizing serialization

Every time I developed an API, I faced the same problem: how to filter the data according to my API calls.

Indeed, some calls that are available for example for a "user" call will not return the same data as a call available for an "administrator".

The project

To start, we will build a REST API for vehicle management, so we will have a "Vehicle " object that will be made up as follows:

  • id: Long
  • brand: String ⇒ The brand, for example (Renault)
  • model: String ⇒ The model, for example (Megane)
  • registrationPlate: String ⇒ The registration plate of the car, for example (SP-800-TT)
  • serialNumber: String ⇒ The serial number of the vehicle

This data can be read by any user. In addition, we will define data that the user does not need, but that the administrator can see:

  • CreatedAt : LocalDatetime ⇒ Date of creation of the object in the database
  • UpdatedAt : LocalDatetime ⇒ Date of modification of the object in database

To save time during my developments, I define an abstract Class which is used as a base for all my entities:

@MappedSuperclass
@Data
@SuperBuilder
@NoArgsConstructor
public abstract class EntityAbstract {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    @CreatedDate
    @Column(nullable = false, columnDefinition = "timestamp default now()")
    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    @JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss'Z'", timezone="UTC")
    LocalDateTime createdAt;

    @LastModifiedDate
    @Column(nullable = false, columnDefinition = "timestamp default now()")
    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    @JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss'Z'", timezone="UTC")
    LocalDateTime updatedAt;

}
Enter fullscreen mode Exit fullscreen mode

Then I define my Vehicle class:

@Entity
@Data
public class Vehicle extends EntityAbstract {


    @NotEmpty
    @Column(nullable = false)
    String registrationPlate;

    @NotEmpty
    @Column(nullable = false)
    String model;

    @NotEmpty
    @Column(nullable = false)
    String brand;

        @NotEmpty
    @Column(nullable = false)
    String serialNumber;
}
Enter fullscreen mode Exit fullscreen mode

Finally we will define a Controller that will allow access to the data using a data retrieval service with a user route and an Admin route:

@RestController
public class VehicleController {
    @Autowired
    private VehicleService vehicleService;

    @GetMapping("/vehicle/list")
    public List<Vehicle> listVehicles() {
        return vehicleService.listVehicles();
    }

    @GetMapping("/admin/vehicle/list")
    public List<Vehicle> adminListVehicles() {
        return vehicleService.listVehicles();
    }
}
Enter fullscreen mode Exit fullscreen mode

For the moment, the rendering of the data is exactly the same, but we will see later how to filter the data.

Jackson to the rescue!

To filter the data, we will use a feature of the Jackson library, the JsonView!
We need to define a class containing interfaces that correspond to the filter levels of our API:

/**
 * Json view filter
 */
public class JsonViews {
    public interface Create {}
    public interface Update extends Create{}
    public interface Summary extends Update{}
    public interface Admin extends Summary{}
}
Enter fullscreen mode Exit fullscreen mode

The "Create " level corresponds to the attributes visible for the creation of an object, "Update" extends the previous filter and will allow to filter only the attributes that can be updated. "Summary" extends the properties of -"Create" and "Update". Finally "Admin" allows you to view or update attributes that are only accessible to Administrators.

This feature is not only used to render a JSON, but also when the API consumes the body of a request.

We will update the entity so that the attributes are filtered correctly by the API:

@MappedSuperclass
@Data
@SuperBuilder
@NoArgsConstructor
public abstract class EntityAbstract {

    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    @JsonView(JsonViews.Update.class)
    private Integer id;

    @CreatedDate
    @Column(nullable = false, columnDefinition = "timestamp default CURRENT_TIMESTAMP")
    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    @JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss'Z'", timezone="UTC")
    @JsonView(JsonViews.Admin.class)
    LocalDateTime createdAt;

    @LastModifiedDate
    @Column(nullable = false, columnDefinition = "timestamp default CURRENT_TIMESTAMP")
    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    @JsonFormat(shape=JsonFormat.Shape.STRING, pattern="yyyy-MM-dd'T'HH:mm:ss'Z'", timezone="UTC")
    @JsonView(JsonViews.Admin.class)
    LocalDateTime updatedAt;

}
Enter fullscreen mode Exit fullscreen mode
@Entity
@Data
public class Vehicle extends EntityAbstract {


    @NotEmpty
    @Column(nullable = false)
    @JsonView(JsonViews.Create.class)
    String registrationPlate;

    @NotEmpty
    @Column(nullable = false)
    @JsonView(JsonViews.Create.class)
    String model;

    @NotEmpty
    @Column(nullable = false)
    @JsonView(JsonViews.Create.class)
    String brand;

    @NotEmpty
    @Column(nullable = false)
    @JsonView(JsonViews.Create.class)
    String serialNumber;
}
Enter fullscreen mode Exit fullscreen mode

Finally we can use it in the controller :

@RestController
public class VehicleController {
    @Autowired
    private VehicleService vehicleService;

        /**
     * List véhicles
     */
    @GetMapping("/vehicle/list")
    @JsonView(JsonViews.Summary.class)
    public List<Vehicle> listVehicles() {
        return vehicleService.listVehicles();
    }

        /**
     * List vehicles (Admin)
     */
    @GetMapping("/admin/vehicle/list")
    @JsonView(JsonViews.Summary.class)
    public List<Vehicle> adminListVehicles() {
        return vehicleService.listVehicles();
    }

        /**
     * Create vehicle (Admin)
     */
    @PostMapping("/admin/vehicle")
    @JsonView(JsonViews.Admin.class)
    public Vehicle postAdminVehicle(@JsonView(JsonViews.Create.class) Vehicle vehicle) {
        return vehicleService.createVehicle(vehicle);
    }

        /**
     * Update vehicle (Admin)
     */
    @PutMapping("/admin/vehicle")
    @JsonView(JsonViews.Admin.class)
    public Vehicle putAdminVehicle(@JsonView(JsonViews.Update.class) Vehicle vehicle) {
        return vehicleService.updateVehicle(vehicle);
    }

}
Enter fullscreen mode Exit fullscreen mode

Results

Vehicle list (User view)

~ curl http://localhost:8080/vehicle/list
[{"id":1,"registrationPlate":"AA-205-AA","model":"205","brand":"PEUGEOT","serialNumber":"3KX9YLH2K980HNYYS1YZ"},{"id":2,"registrationPlate":"AA-206-AA","model":"206","brand":"PEUGEOT","serialNumber":"VST52ASAH145TIZGJ3LC"},{"id":3,"registrationPlate":"AA-207-AA","model":"207","brand":"PEUGEOT","serialNumber":"JI0BNW8D1HYGPFAECEPJ"},{"id":4,"registrationPlate":"AA-208-AA","model":"208","brand":"PEUGEOT","serialNumber":"LN97FF02UWRJYRAEGFL5"},{"id":5,"registrationPlate":"AA-106-AA","model":"106","brand":"PEUGEOT","serialNumber":"R94DJ8P5PT7M6PB5DP5B"},{"id":6,"registrationPlate":"AA-107-AA","model":"107","brand":"PEUGEOT","serialNumber":"OJHHEW2SP2KJDU3CZLFO"},{"id":7,"registrationPlate":"AA-108-AA","model":"108","brand":"PEUGEOT","serialNumber":"49LUMOTWMOQSNHK09MR3"},{"id":8,"registrationPlate":"AA-306-AA","model":"306","brand":"PEUGEOT","serialNumber":"0KBBBHVTZN9P66I8TVGT"},{"id":9,"registrationPlate":"AA-307-AA","model":"307","brand":"PEUGEOT","serialNumber":"TK0GX92MY29AR0H9ZYIB"},{"id":10,"registrationPlate":"AA-308-AA","model":"308","brand":"PEUGEOT","serialNumber":"EKNPA002KCQXYAUX04AV"},{"id":11,"registrationPlate":"AA-309-AA","model":"309","brand":"PEUGEOT","serialNumber":"D1788D08RU0AU3O5BVXR"},{"id":12,"registrationPlate":"AA-405-AA","model":"405","brand":"PEUGEOT","serialNumber":"PHU0M7NKZ799D64VBVJ8"},{"id":13,"registrationPlate":"AA-406-AA","model":"406","brand":"PEUGEOT","serialNumber":"NAD3F2ULL5TY4QCECLAO"},{"id":14,"registrationPlate":"AA-407-AA","model":"407","brand":"PEUGEOT","serialNumber":"C842THXCOBFFZ0O5U0YK"},{"id":15,"registrationPlate":"AA-508-AA","model":"508","brand":"PEUGEOT","serialNumber":"F0T2ELSQ0VPRF1NBY8XH"},{"id":16,"registrationPlate":"AA-407-AA","model":"407","brand":"PEUGEOT","serialNumber":"WEBVP95ON3HDX0TGPFXB"},{"id":17,"registrationPlate":"AA-508-AA","model":"508","brand":"PEUGEOT","serialNumber":"TEZ3D70UDBY8UKHRKSYY"},{"id":18,"registrationPlate":"AA-605-AA","model":"605","brand":"PEUGEOT","serialNumber":"WIGYNFTWH0Q6E85Z2HA2"},{"id":19,"registrationPlate":"AA-607-AA","model":"607","brand":"PEUGEOT","serialNumber":"OLI3JCTF82WUQK14Z295"}]
Enter fullscreen mode Exit fullscreen mode

Vehicle list (Admin view)

~ curl http://localhost:8080/admin/vehicle/list
[{"id":1,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-205-AA","model":"205","brand":"PEUGEOT","serialNumber":"3KX9YLH2K980HNYYS1YZ"},{"id":2,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-206-AA","model":"206","brand":"PEUGEOT","serialNumber":"VST52ASAH145TIZGJ3LC"},{"id":3,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-207-AA","model":"207","brand":"PEUGEOT","serialNumber":"JI0BNW8D1HYGPFAECEPJ"},{"id":4,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-208-AA","model":"208","brand":"PEUGEOT","serialNumber":"LN97FF02UWRJYRAEGFL5"},{"id":5,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-106-AA","model":"106","brand":"PEUGEOT","serialNumber":"R94DJ8P5PT7M6PB5DP5B"},{"id":6,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-107-AA","model":"107","brand":"PEUGEOT","serialNumber":"OJHHEW2SP2KJDU3CZLFO"},{"id":7,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-108-AA","model":"108","brand":"PEUGEOT","serialNumber":"49LUMOTWMOQSNHK09MR3"},{"id":8,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-306-AA","model":"306","brand":"PEUGEOT","serialNumber":"0KBBBHVTZN9P66I8TVGT"},{"id":9,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-307-AA","model":"307","brand":"PEUGEOT","serialNumber":"TK0GX92MY29AR0H9ZYIB"},{"id":10,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-308-AA","model":"308","brand":"PEUGEOT","serialNumber":"EKNPA002KCQXYAUX04AV"},{"id":11,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-309-AA","model":"309","brand":"PEUGEOT","serialNumber":"D1788D08RU0AU3O5BVXR"},{"id":12,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-405-AA","model":"405","brand":"PEUGEOT","serialNumber":"PHU0M7NKZ799D64VBVJ8"},{"id":13,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-406-AA","model":"406","brand":"PEUGEOT","serialNumber":"NAD3F2ULL5TY4QCECLAO"},{"id":14,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-407-AA","model":"407","brand":"PEUGEOT","serialNumber":"C842THXCOBFFZ0O5U0YK"},{"id":15,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-508-AA","model":"508","brand":"PEUGEOT","serialNumber":"F0T2ELSQ0VPRF1NBY8XH"},{"id":16,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-407-AA","model":"407","brand":"PEUGEOT","serialNumber":"WEBVP95ON3HDX0TGPFXB"},{"id":17,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-508-AA","model":"508","brand":"PEUGEOT","serialNumber":"TEZ3D70UDBY8UKHRKSYY"},{"id":18,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-605-AA","model":"605","brand":"PEUGEOT","serialNumber":"WIGYNFTWH0Q6E85Z2HA2"},{"id":19,"createdAt":"2022-02-28T14:32:13Z","updatedAt":"2022-02-28T14:32:13Z","registrationPlate":"AA-607-AA","model":"607","brand":"PEUGEOT","serialNumber":"OLI3JCTF82WUQK14Z295"}]
Enter fullscreen mode Exit fullscreen mode

Create vehicle

~ curl --location --request POST 'localhost:8080/admin/vehicle' \
--header 'Content-Type: application/json' \
--data-raw '{
    "brand": "VolksWagen",
    "model": "Golf",
    "serialNumber": "VWVWVWVWVWVWVW",
    "registrationPlate": "GO-123-LF"
}'
{"id":20,"createdAt":"2022-02-28T15:34:03Z","updatedAt":"2022-02-28T15:34:03Z","registrationPlate":"GO-123-LF","model":"Golf","brand":"VolksWagen","serialNumber":"VWVWVWVWVWVWVW"}
Enter fullscreen mode Exit fullscreen mode

Update vehicle

~ curl --location --request PUT 'localhost:8080/admin/vehicle' \
--header 'Content-Type: application/json' \
--data-raw '{
    "id": 20,
    "brand": "VolksWagen",
    "model": "Golf SW",
    "serialNumber": "VWVWVWVWVWVWVW",
    "registrationPlate": "GO-123-LF"
}'
{"id":20,"createdAt":"2022-02-28T15:40:55Z","updatedAt":"2022-02-28T15:40:55Z","registrationPlate":"GO-123-LF","model":"Golf SW","brand":"VolksWagen","serialNumber":"VWVWVWVWVWVWVW"}
Enter fullscreen mode Exit fullscreen mode

You can find the github of the project here: https://github.com/giboow/jsonview-article

Credits :

Top comments (2)

Collapse
 
martinhaeusler profile image
Martin Häusler

I know that it's very tempting to use JSON views on top of entities, but please don't. Every time I did that I lived to regret it. The problem is that your database schema will evolve over time, and your frontend team will eventually demand additional data being sent over REST (or the same data in a different format). Do yourself a favour for the long run - create DTOs for everything you send via REST. Yes I know they're annoying, yes they impose a maintenance overhead, but they grant you the abillity to evolve your database and your REST interface independently. Just my 5 cents.

Collapse
 
giboow profile image
GiBoOw

Thanks for your feedback Martin.
I agree with you that it is better to use DTOs. That's what I do on big projects. It allows to better control the Inputs and to avoid problems when we make the code evolve.
But when I need an API for a small project and I want to go fast, I find it practical.