НейроАгент

Исправление ошибки несоответствия ID: Spring Boot REST API и клиент Next.js

Узнайте, как устранять ошибки десериализации JSON при вызове REST API Spring Boot из клиента Next.js. Исправьте проблемы несоответствия ID между строками UUID и long ID в клиентах, сгенерированных OpenAPI/Swagger.

Вопрос

Как исправить ошибку несоответствия ID при вызове REST API Spring Boot из клиента Next.js, сгенерированного с помощью OpenAPI/Swagger?

Я столкнулся с ошибкой десериализации JSON при отправке сущности Product с вложенными ProductMaterials в мое REST API Spring Boot. Сообщение об ошибке:

org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `long` from String "52a92362-3c7b-40e6-adfe-85a1007c121f": not a valid `long` value

Вот мои классы сущностей:

java
@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Product {
    @Id
    private String sku;

    @OneToMany(cascade = CascadeType.ALL,
            fetch = FetchType.EAGER,
            targetEntity = ProductMaterial.class)
    private List<ProductMaterial> materials;
}

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ProductMaterial {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @OneToOne
    private Material material;

    private float unitsPerProduct;
}

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Material {
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private String id;

    private String description;
    private float costPerUnit;
    private int inStock;
}

И мои репозитории:

java
@RepositoryRestResource(collectionResourceRel = "product", path = "products")
public interface ProductRepository extends PagingAndSortingRepository<Product, Long>, CrudRepository<Product, Long> {
}

@RepositoryRestResource(collectionResourceRel = "product-material", path = "product-materials", exported = false)
public interface ProductMaterialRepository extends PagingAndSortingRepository<ProductMaterial, String>, CrudRepository<ProductMaterial, String> {
}

@RepositoryRestResource(collectionResourceRel = "material", path = "materials")
public interface MaterialRepository extends PagingAndSortingRepository<Material, String>, CrudRepository<Material, String> {
}

Я использую SpringDoc OpenAPI (springdoc-openapi-starter-webmvc-ui:2.8.13) и сгенерировал TypeScript клиент с помощью:

bash
npx @openapitools/openapi-generator-cli generate -i http://localhost:8080/v3/api-docs -g typescript-fetch -o ./services/generated/api --additional-properties=supportsES6=false,modelPropertyNaming=original,withInterfaces=true,prependFormOrBodyParameters=true

Когда я отправляю продукт с материалами, клиент отправляет этот JSON:

json
{"sku":"PRD-001","name":"Test Produkt","materials":[{"id":-1,"material":{"id":"52a92362-3c7b-40e6-adfe-85a1007c121f","description":"Material 1","costPerUnit":1,"inStock":1},"unitsPerProduct":1},{"id":-1,"material":{"id":"8a0d57cc-d3bd-4653-b50f-d4a14d5183b3","description":"Material 4","costPerUnit":0.25,"inStock":4},"unitsPerProduct":1}],"sellPrice":"1.2"}

Проблема, кажется, заключается в том, что ID материала (UUID строка) неправильно сопоставляется с полем ID ProductMaterial (long). Как я могу решить эту проблему несоответствия ID между клиентом и сервером?

НейроАгент

Ошибка несоответствия ID возникает потому, что ваша спецификация OpenAPI не правильно различает авто-сгенерированный числовой ID ProductMaterial и UUID-ссылку на Material. TypeScript-клиент некорректно отображает UUID-строку Material в поле id ProductMaterial (которое должно быть типа long).

Содержание

Понимание проблемы

Ошибка Cannot deserialize value of type 'long' from String "52a92362-3c7b-40e6-adfe-85a1007c121f": not a valid 'long' value указывает на фундаментальное несоответствие типов между тем, что ожидает ваш Spring Boot API, и тем, что отправляет Next.js клиент.

При рассмотрении ваших классов сущностей:

  • ProductMaterial имеет @Id private long id (авто-сгенерированный числовой ID)
  • Material имеет @Id private String id (UUID-строка)
  • JSON-пayload показывает, что клиент отправляет "id":"52a92362-3c7b-40e6-adfe-85a1007c121f", где ожидается числовое значение

Эта путаница возникает потому, что спецификация OpenAPI не четко различает эти различные типы ID, что приводит к неправильному отображению полей генератором клиента.

Анализ основной причины

Проблема возникает из-за того, как SpringDoc OpenAPI генерирует спецификацию и как интерпретатор OpenAPI клиента обрабатывает ее. Согласно OpenAPI Specification v3.0.3, поля UUID должны быть правильно объявлены с format: "uuid", но это часто происходит автоматически с SpringDoc.

Из нашего исследования мы видим, что это распространенная проблема:

  • Проблема PostgREST #1031 показывает, что спецификации формата UUID часто не правильно преобразуются в клиентский код
  • Проблемы OpenAPI Generator демонстрируют частые проблемы с приведением UUID к строке
  • Вопрос на Stack Overflow показывает правильный способ определения свойств UUID

Решение 1: Исправление спецификации OpenAPI

Наиболее надежным решением является явное определение ваших API-контрактов с правильным форматированием UUID. Добавьте DTO, которые четко разделяют различные типы ID:

java
// Создайте DTO, которые четко разделяют типы ID
public class ProductCreateDTO {
    private String sku;
    private String name;
    private List<ProductMaterialCreateDTO> materials;
    private BigDecimal sellPrice;
}

public class ProductMaterialCreateDTO {
    // Не включайте сюда авто-сгенерированный ID
    private MaterialReferenceDTO material;
    private float unitsPerProduct;
}

public class MaterialReferenceDTO {
    private String id; // Это явно UUID
    private String description;
    private float costPerUnit;
    private int inStock;
}

Затем обновите ваш контроллер для использования этих DTO:

java
@PostMapping("/products")
public ResponseEntity<Product> createProduct(@RequestBody ProductCreateDTO productDTO) {
    // Преобразуйте DTO в сущность и сохраните
    Product product = convertToEntity(productDTO);
    Product savedProduct = productRepository.save(product);
    return ResponseEntity.ok(savedProduct);
}

Согласно OpenAPI Specification, этот подход гарантирует, что спецификация правильно определяет поля UUID с правильным форматом.

Решение 2: Настройка генерации клиента

Измените команду генерации клиента, чтобы включить свойства, которые правильно обрабатывают UUID:

bash
npx @openapitools/openapi-generator-cli generate \
  -i http://localhost:8080/v3/api-docs \
  -g typescript-fetch \
  -o ./services/generated/api \
  --additional-properties=supportsES6=false,modelPropertyNaming=original,withInterfaces=true,prependFormOrBodyParameters=true,typescriptThreePlus=true,useSingleRequestParameter=true,enumNamingConvention=original

Добавьте пользовательскую конфигурацию для правильной обработки UUID в генерации спецификации OpenAPI. Создайте класс конфигурации:

java
@Configuration
public class OpenApiConfig {

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
            .info(new Info()
                .title("Product API")
                .version("1.0")
                .description("API для управления продуктами и материалами"))
            .components(new Components()
                .addSchemas("UUID", new Schema<>()
                    .type("string")
                    .format("uuid")
                    .pattern("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
                    .minLength(36)
                    .maxLength(36)));
    }
}

Это гарантирует, что поля UUID правильно определены в спецификации OpenAPI, что помогает генератору клиента создавать правильные TypeScript типы.

Решение 3: Использование DTO с правильным разделением ID

Создайте отдельные DTO, которые явно обрабатывают проблему отображения ID:

java
// DTO запроса без авто-сгенерированных ID
public class ProductRequest {
    private String sku;
    private String name;
    private List<MaterialReference> materials;
    private String sellPrice;
}

public class MaterialReference {
    private String materialId; // Явная UUID-ссылка
    private float unitsPerProduct;
}

// DTO ответа, которые включают авто-сгенерированные ID
public class ProductResponse {
    private Long id;
    private String sku;
    private String name;
    private List<ProductMaterialResponse> materials;
    private String sellPrice;
}

public class ProductMaterialResponse {
    private Long id; // Авто-сгенерированный числовой ID
    private MaterialResponse material;
    private float unitsPerProduct;
}

Этот подход полностью устраняет путаницу между различными типами ID.

Решение 4: Пользовательская сериализация

Если вам нужно сохранить существующую структуру сущностей, реализуйте пользовательскую сериализацию:

java
public class ProductMaterialSerializer extends JsonSerializer<ProductMaterial> {
    @Override
    public void serialize(ProductMaterial value, JsonGenerator gen, SerializerProvider provider) 
        throws IOException {
        gen.writeStartObject();
        // Не сериализуйте авто-сгенерированный ID для запросов на создание
        // gen.writeNumberField("id", value.getId());
        gen.writeObjectField("material", value.getMaterial());
        gen.writeNumberField("unitsPerProduct", value.getUnitsPerProduct());
        gen.writeEndObject();
    }
}

// Используйте с @JsonSerialize в списке вашей сущности
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, targetEntity = ProductMaterial.class)
@JsonSerialize(contentUsing = ProductMaterialSerializer.class)
private List<ProductMaterial> materials;

Лучшие практики для работы с UUID

На основе результатов исследования из OpenAPI Specification и различных проблем на GitHub, вот лучшие практики:

  1. Всегда указывайте формат UUID в вашей спецификации OpenAPI:

    yaml
    id:
      type: string
      format: uuid
      pattern: '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
      minLength: 36
      maxLength: 36
    
  2. Используйте отдельные DTO для запросов и ответов, чтобы избежать путаницы с ID

  3. Настройте генератор клиента с соответствующими свойствами для обработки UUID

  4. Рассмотрите возможность использования UUID последовательно или четкого разделения числовых и строковых ID в дизайне вашего API

  5. Тестируйте вашу спецификацию API с помощью инструментов вроде Swagger UI, чтобы убедиться, что поля UUID правильно отображаются и проверяются

Заключение

Ошибка несоответствия ID между вашим Spring Boot API и Next.js клиентом может быть решена несколькими способами:

  1. Исправьте спецификацию OpenAPI, явно определив форматы UUID и используя правильные DTO
  2. Настройте конфигурацию генерации клиента для правильной обработки UUID
  3. Используйте отдельные DTO, которые четко различают различные типы ID
  4. Реализуйте пользовательскую сериализацию, когда вам нужно сохранить существующие структуры сущностей

Наиболее устойчивым решением является реализация правильных DTO с четким разделением ID и обеспечение правильного определения полей UUID в вашей спецификации OpenAPI с соответствующим форматом. Этот подход предотвращает подобные проблемы в будущем и делает ваш API более поддерживаемым.

Не забудьте проверить вашу спецификацию OpenAPI с помощью инструментов вроде Swagger UI, чтобы убедиться, что все поля UUID правильно определены перед генерацией клиентского кода.

Источники

  1. OpenAPI Specification v3.0.3 - UUID Format Definition
  2. How to define UUID property in JSON Schema and Open API (OAS)
  3. OpenAPI format uuid - PostgREST Issue #1031
  4. ClassCastException when casting UUIDs to strings - OpenAPI Generator Issue #16429
  5. UUID parameters don’t have format “uuid” - SpringFox Issue #2314