Как исправить ошибку несоответствия 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
Вот мои классы сущностей:
@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;
}
И мои репозитории:
@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 клиент с помощью:
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:
{"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).
Содержание
- Понимание проблемы
- Анализ основной причины
- Решение 1: Исправление спецификации OpenAPI
- Решение 2: Настройка генерации клиента
- Решение 3: Использование DTO с правильным разделением ID
- Решение 4: Пользовательская сериализация
- Лучшие практики для работы с UUID
- Заключение
Понимание проблемы
Ошибка 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:
// Создайте 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:
@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:
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. Создайте класс конфигурации:
@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:
// 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: Пользовательская сериализация
Если вам нужно сохранить существующую структуру сущностей, реализуйте пользовательскую сериализацию:
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, вот лучшие практики:
-
Всегда указывайте формат UUID в вашей спецификации OpenAPI:
yamlid: 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 -
Используйте отдельные DTO для запросов и ответов, чтобы избежать путаницы с ID
-
Настройте генератор клиента с соответствующими свойствами для обработки UUID
-
Рассмотрите возможность использования UUID последовательно или четкого разделения числовых и строковых ID в дизайне вашего API
-
Тестируйте вашу спецификацию API с помощью инструментов вроде Swagger UI, чтобы убедиться, что поля UUID правильно отображаются и проверяются
Заключение
Ошибка несоответствия ID между вашим Spring Boot API и Next.js клиентом может быть решена несколькими способами:
- Исправьте спецификацию OpenAPI, явно определив форматы UUID и используя правильные DTO
- Настройте конфигурацию генерации клиента для правильной обработки UUID
- Используйте отдельные DTO, которые четко различают различные типы ID
- Реализуйте пользовательскую сериализацию, когда вам нужно сохранить существующие структуры сущностей
Наиболее устойчивым решением является реализация правильных DTO с четким разделением ID и обеспечение правильного определения полей UUID в вашей спецификации OpenAPI с соответствующим форматом. Этот подход предотвращает подобные проблемы в будущем и делает ваш API более поддерживаемым.
Не забудьте проверить вашу спецификацию OpenAPI с помощью инструментов вроде Swagger UI, чтобы убедиться, что все поля UUID правильно определены перед генерацией клиентского кода.
Источники
- OpenAPI Specification v3.0.3 - UUID Format Definition
- How to define UUID property in JSON Schema and Open API (OAS)
- OpenAPI format uuid - PostgREST Issue #1031
- ClassCastException when casting UUIDs to strings - OpenAPI Generator Issue #16429
- UUID parameters don’t have format “uuid” - SpringFox Issue #2314