admin管理员组

文章数量:1200344

I'm following Spring training with Java and decided to replicate the content of each class in a Kotlin project as well. However, I came across an error in one of the entity cascade validation steps using the Javax Validation @ConvertGroup annotation in the project made in Kotlin, which does not happen in Java.

The project has a Restaurant entity that has a @ManyToOne relationship with the Cuisine entity and that will have its attributes validated separately without the validation of one affecting the validation of the other. However, after applying these validations, I can register a Restaurant normally in the Java project, but in the Kotlin project I receive an error saying that cuisine.name cannot be blank, which should not happen.

// Payload for Restaurant registration
{
    "name": "Thai Delivery",
    "deliveryFee": 12,
    "cuisine": {
        "id": 1
    }
}

Below are the respective codes in each language:

// Groups.java
package br.dev.s2w.jfoods.api;

public interface Groups {
    public interface CuisineId {}
    }
// Groups.kt
package br.dev.s2w.kfoods.api

interface Groups {
    interface CuisineId
}
// Restaurant.java
package br.dev.s2w.jfoods.api.domain.model;

import br.dev.s2w.jfoods.api.Groups;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;

import javax.persistence.*;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.PositiveOrZero;
import javax.validation.groups.ConvertGroup;
import javax.validation.groups.Default;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@Entity
public class Restaurant {
    @EqualsAndHashCode.Include
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Column(nullable = false)
    private String name;

    @PositiveOrZero
    @Column(name = "delivery_fee", nullable = false)
    private BigDecimal deliveryFee;

    @Valid
    @ConvertGroup(from = Default.class, to = Groups.CuisineId.class)
    @NotNull
    @ManyToOne
    @JoinColumn(name = "cuisine_id", nullable = false)
    private Cuisine cuisine;

    @JsonIgnore
    @Embedded
    private Address address;

    @JsonIgnore
    @CreationTimestamp
    @Column(nullable = false, columnDefinition = "datetime")
    private LocalDateTime registrationDate;

    @JsonIgnore
    @UpdateTimestamp
    @Column(nullable = false, columnDefinition = "datetime")
    private LocalDateTime lastUpdateDate;

    @JsonIgnore
    @ManyToMany
    @JoinTable(name = "restaurant_payment_method",
            joinColumns = @JoinColumn(name = "restaurant_id"),
            inverseJoinColumns = @JoinColumn(name = "payment_method_id"))
    private List<PaymentMethod> paymentMethods = new ArrayList<>();

    @JsonIgnore
    @OneToMany(mappedBy = "restaurant")
    private List<Product> products = new ArrayList<>();
}
// Restaurant.kt
package br.dev.s2w.kfoods.api.domain.model

import br.dev.s2w.kfoods.api.Groups
import com.fasterxml.jackson.annotation.JsonIgnore
import org.hibernate.annotations.CreationTimestamp
import org.hibernate.annotations.UpdateTimestamp
import java.math.BigDecimal
import java.time.LocalDateTime
import javax.persistence.*
import javax.validation.Valid
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull
import javax.validation.constraints.PositiveOrZero
import javax.validation.groups.ConvertGroup
import javax.validation.groups.Default

@Entity
data class Restaurant(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null,

    @field:NotBlank
    @Column(nullable = false)
    var name: String? = null,

    @field:PositiveOrZero
    @Column(name = "delivery_fee", nullable = false)
    var deliveryFee: BigDecimal? = null,

    @field:Valid
    @field:ConvertGroup(from = Default::class, to = Groups.CuisineId::class)
    @field:NotNull
    @ManyToOne
    @JoinColumn(name = "cuisine_id", nullable = false)
    var cuisine: Cuisine? = null,

    @Embedded
    @JsonIgnore
    var address: Address? = null,

    @CreationTimestamp
    @Column(nullable = false, columnDefinition = "datetime")
    @JsonIgnore
    var registrationDate: LocalDateTime? = null,

    @UpdateTimestamp
    @Column(nullable = false, columnDefinition = "datetime")
    @JsonIgnore
    var lastUpdateDate: LocalDateTime? = null,

    @ManyToMany
    @JoinTable(
        name = "restaurant_payment_method",
        joinColumns = [JoinColumn(name = "restaurant_id")],
        inverseJoinColumns = [JoinColumn(name = "payment_method_id")]
    )
    @JsonIgnore
    var paymentMethods: MutableList<PaymentMethod> = mutableListOf(),

    @OneToMany(mappedBy = "restaurant")
    @JsonIgnore
    var products: MutableList<Product> = mutableListOf()
)
// Cuisine.java
package br.dev.s2w.jfoods.api.domain.model;

import br.dev.s2w.jfoods.api.Groups;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.EqualsAndHashCode;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;

@Data
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@Entity
public class Cuisine {
    @NotNull(groups = Groups.CuisineId.class)
    @EqualsAndHashCode.Include
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotBlank
    @Column(nullable = false)
    private String name;

    @JsonIgnore
    @OneToMany(mappedBy = "cuisine")
    private List<Restaurant> restaurants = new ArrayList<>();
}
// Cuisine.kt
package br.dev.s2w.kfoods.api.domain.model

import br.dev.s2w.kfoods.api.Groups
import com.fasterxml.jackson.annotation.JsonIgnore
import javax.persistence.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.NotNull

@Entity
data class Cuisine(
    @field:NotNull(groups = [Groups.CuisineId::class])
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null,

    @field:NotBlank
    @Column(nullable = false)
    var name: String? = null,

    @OneToMany(mappedBy = "cuisine")
    @JsonIgnore
    var restaurants: MutableList<Restaurant> = mutableListOf()
)
// POST /restaurants
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Restaurant add(@RequestBody @Valid Restaurant restaurant) {
        try {
            return restaurantRegister.save(restaurant);
        } catch (CuisineNotFoundException e) {
            throw new BusinessException(e.getMessage(), e);
        }
    }
// POST /restaurants
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    fun add(@RequestBody @Valid restaurant: Restaurant): Restaurant =
        try {
            restaurantRegister.save(restaurant)
        } catch (e: CuisineNotFoundException) {
            throw BusinessException(e.message, e)
        }

Yeah! I will make all attributes in Kotlin immutable and handle the ones that cannot receive a null value.

Response to each call:

// Java Status: 201 Created Response:

{
    "id": 7,
    "name": "Thai Delivery",
    "deliveryFee": 12,
    "cuisine": {
        "id": 1,
        "name": "Thai"
    }
}

// Kotlin Status: 400 Bad Request Response:

{
    "timestamp": "2025-01-21T22:35:14.4081533",
    "status": 400,
    "type": ";,
    "title": "Invalid data",
    "detail": "One or more fields are invalid. Fill in correctly and try again!",
    "userMessage": "One or more fields are invalid. Fill in correctly and try again!",
    "fields": [
        {
            "name": "cuisine.name",
            "userMessage": "must not be blank"
        }
    ]
}

// Kotlin/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns=".0.0" xmlns:xsi=";
         xsi:schemaLocation=".0.0 .0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>br.dev</groupId>
    <artifactId>s2w-kfoods-api</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>s2w-kfoods-api</name>
    <description>Your favorite food delivery service!</description>

    <developers>
        <developer>
            <id>SW</id>
            <name>Wybson Santana</name>
            <url>/</url>
        </developer>
    </developers>

    <properties>
        <java.version>17</java.version>
        <kotlin.version>1.8.22</kotlin.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.module</groupId>
            <artifactId>jackson-module-kotlin</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-reflect</artifactId>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-stdlib</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-core</artifactId>
            <version>9.8.3</version>
        </dependency>
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-mysql</artifactId>
            <version>9.8.3</version>
        </dependency>
        <dependency>
            <groupId>org.apachemons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jetbrains.kotlin</groupId>
            <artifactId>kotlin-test-junit5</artifactId>
            <version>${kotlin.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
        <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.jetbrains.kotlin</groupId>
                <artifactId>kotlin-maven-plugin</artifactId>
                <configuration>
                    <args>
                        <arg>-Xjsr305=strict</arg>
                    </args>
                    <compilerPlugins>
                        <plugin>spring</plugin>
                        <plugin>jpa</plugin>
                        <plugin>no-arg</plugin>
                    </compilerPlugins>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>org.jetbrains.kotlin</groupId>
                        <artifactId>kotlin-maven-allopen</artifactId>
                        <version>${kotlin.version}</version>
                    </dependency>
                    <dependency>
                        <groupId>org.jetbrains.kotlin</groupId>
                        <artifactId>kotlin-maven-noarg</artifactId>
                        <version>${kotlin.version}</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>

</project>

I tested passing the annotations with and without field:, get:, set: etc, as well as changing the versions of Spring (not in the 3.x + range), Java, Kotlin and dependencies, but nothing had any practical effect.

I believe it could be a bug in Kotlin, as I found an issue about something similar in a Quarkus project: Validation groups for REST endpoint does not work with Kotlin · Issue #20395 · quarkusio/quarkus

Any kind of help and/or possible solution will be greatly appreciated!

Thank you very much!

本文标签: