НейроАгент

Исправление несоответствия UUID ID: Интеграция API Spring Boot и Next.js

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

Вопрос

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

Я столкнулся с ошибкой разбора JSON при отправке продукта с материалами из клиента Next.js в REST API Spring Boot. Сообщение об ошибке: “Не удалось десериализовать значение типа long из String “52a92362-3c7b-40e6-adfe-85a1007c121f”: недопустимое значение long”.

В моем приложении Spring Boot есть следующие сущности:

  • Product (с String SKU в качестве ID и отношением OneToMany к ProductMaterial)
  • ProductMaterial (с auto-generated ID типа long и отношением OneToOne к Material)
  • Material (с UUID в качестве ID)

Я настроил репозитории с Spring Data REST и сгенерировал документацию OpenAPI с помощью SpringDoc. Затем я сгенерировал TypeScript-клиент с помощью OpenAPI Generator с командой:
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

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

НейроАгент

Проблема, с которой вы столкнулись, вызвана несоответствием типов между UUID Material ID на сервере Spring Boot и тем, как генератор OpenAPI интерпретирует и создает TypeScript-клиент. Когда ваш Next.js-клиент отправляет UUID в виде строки, бэкенд Spring Boot пытается десериализовать его как long, что вызывает ошибку парсинга JSON.

Существует несколько решений для устранения этого несоответствия типов, от изменения спецификации OpenAPI до настройки конфигурации Spring Boot.


Содержание


Понимание основной причины

Основная проблема заключается в том, как генератор OpenAPI интерпретирует ваши поля UUID и генерирует TypeScript-типы. Когда Spring Boot через SpringDoc предоставляет доступ к вашим сущностям, поля UUID могут быть недостаточно хорошо аннотированы, что заставляет генератор по умолчанию использовать базовые типы, такие как string или number.

Как демонстрирует Stack Overflow, поля UUID должны явно определяться как строки с соответствующими аннотациями формата в спецификации OpenAPI. Без этих аннотаций генератор может корректно не обрабатывать преобразования UUID в тип long.


Решение 1: Изменение спецификации OpenAPI

Наиболее надежный подход — явно определить поля UUID в вашей спецификации OpenAPI с правильными аннотациями типов.

Добавление определений схемы UUID

Сначала добавьте определение схемы UUID в конфигурацию OpenAPI:

java
@Configuration
public class OpenApiConfig {
    
    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
            .info(new Info()
                .title("Product API")
                .version("1.0"))
            .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:

java
@Entity
public class Material {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    @Schema(description = "UUID материала", 
            format = "uuid",
            pattern = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$")
    private UUID id;
    
    // другие поля...
}

Этот подход гарантирует, что генератор OpenAPI правильно идентифицирует поля UUID и генерирует соответствующие TypeScript-типы.


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

Если изменение спецификации OpenAPI не представляется возможным, вы можете настроить сериализацию Spring Boot для корректной обработки преобразований UUID в long.

Создание кастомного десериализатора

java
public class UuidToLongDeserializer extends JsonDeserializer<Long> {
    
    @Override
    public Long deserialize(JsonParser p, DeserializationContext ctxt) 
        throws IOException, JsonProcessingException {
        
        String value = p.getValueAsString();
        try {
            // Если это строка UUID, преобразуем в long
            if (value.contains("-")) {
                UUID uuid = UUID.fromString(value);
                return uuid.getMostSignificantBits();
            }
            // Иначе, парсим как long напрямую
            return Long.parseLong(value);
        } catch (IllegalArgumentException e) {
            throw ctxt.instantiationException(Long.class, 
                "Недопустимое значение UUID или long: " + value);
        }
    }
}

Применение кастомного десериализатора

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

java
@Entity
public class ProductMaterial {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    @JsonDeserialize(using = UuidToLongDeserializer.class)
    private long id;
    
    // другие поля...
}

Это решение позволяет вашему приложению Spring Boot корректно обрабатывать как строки UUID, так и значения long для поля Material ID.


Решение 3: Корректировка генерации TypeScript-клиента

Измените команду генератора OpenAPI для включения лучшей обработки 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 \
  --additional-properties=typescriptThreePlus=true,useDateType=true,useEnumType=true

Если вы по-прежнему сталкиваетесь с проблемами, вы можете создать кастомный шаблон или пост-обработать сгенерированный TypeScript-клиент, чтобы убедиться, что поля UUID правильно типизированы как string, а не number.


Решение 4: Создание кастомных DTO

Создайте кастомные объекты передачи данных (DTO), которые явно определяют правильные типы для вашего API:

java
public class MaterialDto {
    
    @Schema(description = "UUID материала", format = "uuid")
    private String id;
    
    // другие поля с правильными типами...
}

public class ProductMaterialDto {
    
    @Schema(description = "UUID ссылки на материал")
    private String materialId;
    
    // другие поля...
}

Используйте эти DTO в ваших контроллерах вместо прямого раскрытия сущностей:

java
@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    @PostMapping
    public ResponseEntity<Product> createProduct(@RequestBody ProductDto productDto) {
        // Преобразуем DTO в сущность и сохраняем
        Product product = convertToEntity(productDto);
        Product savedProduct = productService.save(product);
        return ResponseEntity.ok(savedProduct);
    }
    
    private Product convertToEntity(ProductDto dto) {
        // Логика преобразования, включая обработку UUID
    }
}

Этот подход дает полный контроль над тем, как ваши данные сериализуются и десериализуются.


Решение 5: Использование JSON Views

JSON Views Spring Boot могут помочь управлять сериализацией на основе различных сценариев использования:

java
public class Views {
    public interface Public {}
    public interface Internal extends Public {}
}

@Entity
public class Material {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @JsonView(Views.Public.class)
    private UUID id;
    
    @JsonView(Views.Internal.class)
    private long internalId;
    
    // другие поля...
}

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

java
@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    @PostMapping
    public ResponseEntity<Product> createProduct(
        @RequestBody @JsonView(Views.Public.class) ProductDto productDto) {
        // Обработка запроса с публичным видом
    }
}

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

1. Всегда правильно форматировать UUID

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

yaml
schemas:
  UUID:
    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. Использовать value objects для UUID

Рассмотрите возможность реализации UUID как value object для лучшей безопасности типов:

java
public class MaterialId {
    private final UUID value;
    
    public MaterialId(UUID value) {
        this.value = Objects.requireNonNull(value);
    }
    
    public UUID getValue() {
        return value;
    }
    
    @Override
    public String toString() {
        return value.toString();
    }
}

3. Настроить Jackson для работы с UUID

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

java
@Configuration
public class JacksonConfig {
    
    @Bean
    public Module uuidModule() {
        SimpleModule module = new SimpleModule();
        module.addSerializer(UUID.class, new StdSerializer<UUID>(UUID.class) {
            @Override
            public void serialize(UUID value, JsonGenerator gen, SerializerProvider provider) 
                throws IOException {
                gen.writeString(value.toString());
            }
        });
        module.addDeserializer(UUID.class, new StdDeserializer<UUID>(UUID.class) {
            @Override
            public UUID deserialize(JsonParser p, DeserializationContext ctxt) 
                throws IOException {
                return UUID.fromString(p.getValueAsString());
            }
        });
        return module;
    }
}

4. Валидировать генерацию OpenAPI

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


Заключение

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

  1. Изменение спецификации OpenAPI: Явно определите поля UUID с правильными аннотациями формата для обеспечения корректной генерации TypeScript.

  2. Настройка сериализации: Реализуйте кастомные десериализаторы в Spring Boot для корректной обработки как строк UUID, так и значений long.

  3. Корректировка генерации клиента: Измените параметры генератора OpenAPI или выполните пост-обработку сгенерированного TypeScript-клиента для правильной типизации UUID.

  4. Использование кастомных DTO: Создайте отдельные объекты передачи данных с явными определениями типов для контроля сериализации API.

  5. Реализация JSON Views: Используйте JSON Views Spring Boot для контроля того, какие поля раскрываются в различных контекстах API.

В большинстве случаев Решение 1 (изменение спецификации OpenAPI) предоставляет наиболее надежный и поддерживаемый подход, так как оно устраняет корневую причину на уровне спецификации. Однако для вашего конкретного случая использования может дать лучшие результаты комбинация нескольких решений.

Всегда тщательно тестируйте ваши конечные точки API после внесения любых изменений, чтобы обеспечить обратную совместимость и правильную обработку как строк UUID, так и числовых ID.