NeuroAgent

Fix UUID ID Mismatch: Spring Boot & Next.js API Integration

Resolve JSON parse errors when integrating Next.js client with Spring Boot REST API. Learn to fix UUID-to-long type mismatches in OpenAPI/Swagger generated clients.

Question

How to resolve ID mismatch error when calling Spring Boot REST API from Next.js client generated using OpenAPI/Swagger?

I’m encountering a JSON parse error when posting a product with materials from a Next.js client to a Spring Boot REST API. The error message is: “Cannot deserialize value of type long from String “52a92362-3c7b-40e6-adfe-85a1007c121f”: not a valid long value”.

My Spring Boot application has the following entities:

  • Product (with String SKU as ID and a OneToMany relationship to ProductMaterial)
  • ProductMaterial (with long auto-generated ID and a OneToOne relationship to Material)
  • Material (with UUID as ID)

I’ve configured repositories with Spring Data REST and generated OpenAPI documentation using SpringDoc. I then generated a TypeScript client using OpenAPI Generator with the command:
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

When I try to post a product with materials, the client sends a request where the material ID is a UUID string, but the server expects a long. How can I resolve this type mismatch between the client and server?

NeuroAgent

The issue you’re experiencing stems from a type mismatch between your UUID Material ID on the Spring Boot server and how the OpenAPI generator interprets and generates the TypeScript client. When your Next.js client sends a UUID string, the Spring Boot backend tries to deserialize it as a long, causing the JSON parse error.

Several solutions exist to resolve this type mismatch, ranging from modifying your OpenAPI specification to adjusting your Spring Boot configuration.


Contents


Understanding the Root Cause

The core issue lies in how the OpenAPI generator interprets your UUID fields and generates TypeScript types. When Spring Boot exposes your entities through SpringDoc, the UUID fields might not be properly annotated, causing the generator to default to basic types like string or number.

As Stack Overflow demonstrates, UUID fields should explicitly be defined as strings with proper format annotations in the OpenAPI specification. Without these annotations, the generator may not correctly handle UUID-to-long type conversions.


Solution 1: Modify OpenAPI Specification

The most reliable approach is to explicitly define UUID fields in your OpenAPI specification with proper type annotations.

Add UUID Schema Definitions

First, add a UUID schema definition to your OpenAPI configuration:

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)));
    }
}

Annotate Entity Fields

Annotate your UUID fields with proper OpenAPI documentation:

java
@Entity
public class Material {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    @Schema(description = "Material 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;
    
    // other fields...
}

This approach ensures that the OpenAPI generator correctly identifies UUID fields and generates appropriate TypeScript types.


Solution 2: Customize Spring Boot Serialization

If modifying the OpenAPI specification isn’t feasible, you can customize your Spring Boot serialization to handle UUID-to-long conversions gracefully.

Create Custom Deserializer

java
public class UuidToLongDeserializer extends JsonDeserializer<Long> {
    
    @Override
    public Long deserialize(JsonParser p, DeserializationContext ctxt) 
        throws IOException, JsonProcessingException {
        
        String value = p.getValueAsString();
        try {
            // If it's a UUID string, convert to long representation
            if (value.contains("-")) {
                UUID uuid = UUID.fromString(value);
                return uuid.getMostSignificantBits();
            }
            // Otherwise, parse as long directly
            return Long.parseLong(value);
        } catch (IllegalArgumentException e) {
            throw ctxt.instantiationException(Long.class, 
                "Invalid UUID or long value: " + value);
        }
    }
}

Apply Custom Deserializer

Apply the custom deserializer to your entity:

java
@Entity
public class ProductMaterial {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    @JsonDeserialize(using = UuidToLongDeserializer.class)
    private long id;
    
    // other fields...
}

This solution allows your Spring Boot application to handle both UUID strings and long values for the Material ID field.


Solution 3: Adjust TypeScript Client Generation

Modify your OpenAPI generator command to include better UUID handling:

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

If you still encounter issues, you can create a custom template or post-process the generated TypeScript client to ensure UUID fields are properly typed as string rather than number.


Solution 4: Create Custom DTOs

Create custom Data Transfer Objects that explicitly define the correct types for your API:

java
public class MaterialDto {
    
    @Schema(description = "Material UUID", format = "uuid")
    private String id;
    
    // other fields with proper types...
}

public class ProductMaterialDto {
    
    @Schema(description = "Material reference UUID")
    private String materialId;
    
    // other fields...
}

Use these DTOs in your controllers instead of directly exposing entities:

java
@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    @PostMapping
    public ResponseEntity<Product> createProduct(@RequestBody ProductDto productDto) {
        // Convert DTO to entity and save
        Product product = convertToEntity(productDto);
        Product savedProduct = productService.save(product);
        return ResponseEntity.ok(savedProduct);
    }
    
    private Product convertToEntity(ProductDto dto) {
        // Conversion logic including UUID handling
    }
}

This approach provides complete control over how your data is serialized and deserialized.


Solution 5: Use JSON Views

Spring Boot’s JSON Views can help control serialization based on different use cases:

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;
    
    // other fields...
}

Apply the appropriate view in your controllers:

java
@RestController
@RequestMapping("/api/products")
public class ProductController {
    
    @PostMapping
    public ResponseEntity<Product> createProduct(
        @RequestBody @JsonView(Views.Public.class) ProductDto productDto) {
        // Handle request with public view
    }
}

Best Practices for UUID Handling

1. Always Format UUID Properly

Ensure UUID fields are consistently formatted as strings with proper regex validation:

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. Use UUID Value Objects

Consider implementing UUID as a value object for better type safety:

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. Configure Jackson for UUID Handling

Add Jackson configuration for proper UUID serialization:

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. Validate OpenAPI Generation

Before generating the TypeScript client, validate your OpenAPI specification using tools like Swagger UI or Redoc to ensure all UUID fields are properly defined.


Conclusion

The ID mismatch error between your Next.js client and Spring Boot API can be resolved through several approaches:

  1. Modify OpenAPI Specification: Explicitly define UUID fields with proper format annotations to ensure correct TypeScript generation.

  2. Customize Serialization: Implement custom deserializers in Spring Boot to handle both UUID strings and long values gracefully.

  3. Adjust Client Generation: Modify OpenAPI generator parameters or post-process the generated TypeScript client for proper UUID typing.

  4. Use Custom DTOs: Create separate data transfer objects with explicit type definitions to control API serialization.

  5. Implement JSON Views: Use Spring Boot’s JSON Views to control which fields are exposed in different API contexts.

For most cases, Solution 1 (modifying the OpenAPI specification) provides the most robust and maintainable approach, as it addresses the root cause at the specification level. However, combining multiple solutions may yield the best results for your specific use case.

Always remember to thoroughly test your API endpoints after implementing any changes to ensure backward compatibility and proper handling of both UUID strings and numeric IDs.