SpringBoot - Contextualizing serialization

giboow

GiBoOw

Posted on February 28, 2022

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 :

💖 💪 🙅 🚩
giboow
GiBoOw

Posted on February 28, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

SpringBoot - Contextualizing serialization
springboot SpringBoot - Contextualizing serialization

February 28, 2022