Spring Boot JSON Serialization: Solving String vs Long Type Inconsistency

🌏 閱讀中文版本

Why JSON Serialization Consistency Matters

1. Frontend Parsing and Type Safety

Inconsistent number types in API responses can cause serious frontend issues. For example:

  • TypeScript type checking failures: Fields defined as number receive string values
  • Incorrect calculations: "100" + 50 results in "10050" instead of 150
  • Conditional logic errors: "0" == false is true, but "0" === false is false

2. Cross-Environment Consistency

API responses should remain identical across development, testing, and production environments. Different serialization settings can lead to:

  • Tests passing in staging but failing in production
  • Mismatch between frontend mock data and actual API responses
  • Data type conflicts when integrating with third-party systems

3. API Stability and Backward Compatibility

Once an API goes public, changing response formats breaks existing clients. Unified serialization settings ensure:

  • Version upgrades don’t unexpectedly alter JSON format
  • Data exchange between multiple services stays consistent
  • Fewer client errors caused by type mismatches

Problem Background

When developing Spring Boot applications, we frequently serialize objects to JSON for data transmission or storage. However, JSON response data types may vary across different environments.

Typical problem scenario:

  • Environment A: Java API serializes Long numbers as "12345" (string)
  • Environment B: Same field serializes as 12345 (number)

This inconsistency can cause frontend parsing errors, TypeScript type check failures, or integration issues with other systems. Understanding and resolving this issue is crucial for maintaining system stability and consistency.


Jackson Serialization Configuration

Jackson is Spring Boot’s default JSON serialization/deserialization library. Its behavior can be adjusted through configuration files or code. Here are the key settings that affect how numeric data is represented:

Key Configuration Options

Configuration Option Default Value Purpose Impact
WRITE_DATES_AS_TIMESTAMPS true Serialize dates as timestamps Affects Date, LocalDateTime, etc.
WRITE_NUMBERS_AS_STRINGS false Serialize numbers as strings Affects Long, Integer, Double, etc.

Important Note: Jackson’s default behavior serializes numbers to their native JSON types (e.g., Long becomes a JSON number), not strings. If Environment A produces string types, it means WRITE_NUMBERS_AS_STRINGS=true has been enabled there.


Configuration Methods

Method 1: application.properties Configuration

# Date serialization (false = ISO-8601 string format, true = Unix timestamp)
spring.jackson.serialization.WRITE_DATES_AS_TIMESTAMPS=false

# Number serialization (false = numeric type, true = string type)
spring.jackson.serialization.WRITE_NUMBERS_AS_STRINGS=false

Method 2: application.yml Configuration

spring:
  jackson:
    serialization:
      WRITE_DATES_AS_TIMESTAMPS: false  # Use ISO-8601 format for dates
      WRITE_NUMBERS_AS_STRINGS: false   # Keep numbers in native type

Method 3: Custom ObjectMapper (Code Configuration)

Alternatively, you can customize ObjectMapper directly in Java code. This ensures identical Jackson configuration across all environments:

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JacksonConfig {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();

        // Serialize dates as ISO-8601 format instead of Unix timestamps
        mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);

        // Keep numbers in native type (Long, Integer, etc.) instead of strings
        mapper.configure(SerializationFeature.WRITE_NUMBERS_AS_STRINGS, false);

        return mapper;
    }
}

Default Behavior and Overrides

Jackson Default Serialization Behavior

  • Numeric types: Retain native type (Long → JSON number)
  • Date types: Serialize as Unix timestamp (1672531200000)

How to Enable String Serialization for Numbers

To serialize numbers as strings (generally not recommended), explicitly enable:

spring.jackson.serialization.WRITE_NUMBERS_AS_STRINGS=true

Serialization result example:

{
  "userId": "12345",      // With WRITE_NUMBERS_AS_STRINGS enabled
  "balance": "1000.50",   // All numbers become strings
  "timestamp": 1672531200000  // Dates remain timestamps (unless configured otherwise)
}

Best Practices

  1. Unify configuration across all environments
    • Explicitly set values in application.properties or application.yml
    • Avoid relying on defaults to ensure configuration visibility
  2. Version control your configuration files
    • Include Jackson configuration in Git version control
    • Ensure dev, test, and production use identical settings
  3. Prefer numeric types
    • Keep WRITE_NUMBERS_AS_STRINGS=false (default value)
    • Avoid unnecessary type conversion overhead
  4. Use ISO-8601 format for dates
    • Set WRITE_DATES_AS_TIMESTAMPS=false
    • Improves readability and cross-system compatibility
  5. Validate with integration tests
    • Write test cases to verify JSON serialization results
    • Ensure types match API specifications

Frequently Asked Questions

Q1: Why does Environment A serialize as String while Environment B uses Long?

A: The most common reason is different application.properties settings between environments. Check the following:

  • Environment A may have WRITE_NUMBERS_AS_STRINGS=true
  • Environment B uses the default value (false)
  • Or both environments use different versions of the Jackson library

Solution: Unify configuration files and ensure all environments use the same dependency versions.

Q2: What is the default value of WRITE_NUMBERS_AS_STRINGS?

A: The default is false, meaning Jackson serializes numbers to JSON numeric types, not strings. If your environment produces string serialization, it means this setting was explicitly changed to true.

Q3: How can I verify the current environment’s serialization settings?

A: You can check using the following approach:

@Autowired
private ObjectMapper objectMapper;

public void checkConfig() {
    boolean numbersAsStrings = objectMapper.isEnabled(SerializationFeature.WRITE_NUMBERS_AS_STRINGS);
    boolean datesAsTimestamps = objectMapper.isEnabled(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

    System.out.println("WRITE_NUMBERS_AS_STRINGS: " + numbersAsStrings);
    System.out.println("WRITE_DATES_AS_TIMESTAMPS: " + datesAsTimestamps);
}

Q4: Can I customize serialization for specific fields?

A: Yes, use @JsonSerialize or @JsonFormat annotations to customize serialization behavior for specific fields:

public class UserResponse {
    @JsonSerialize(using = ToStringSerializer.class)
    private Long userId;  // Force serialize as string

    @JsonFormat(shape = JsonFormat.Shape.NUMBER)
    private Long balance;  // Force serialize as number
}

Q5: How can I safely change settings in a running production environment?

A: We recommend a gradual rollout strategy:

  1. API versioning: Create a new API version (e.g., /v2/users) with the new serialization settings
  2. Backward compatibility: Keep the old API version, giving clients time to migrate
  3. Gradual transition: Monitor error rates and confirm no impact before full cutover
  4. Documentation updates: Clearly inform API users of type changes

Q6: How can I test serialization results?

A: Write integration tests to verify JSON format:

@SpringBootTest
@AutoConfigureMockMvc
public class SerializationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testUserResponseSerialization() throws Exception {
        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.userId").isNumber())  // Verify numeric type
            .andExpect(jsonPath("$.balance").isNumber());
    }
}

Troubleshooting

Issue: Set WRITE_NUMBERS_AS_STRINGS=false but still serializing as strings

Possible causes:

  • Field uses @JsonSerialize(using = ToStringSerializer.class) annotation
  • Custom ObjectMapper bean overrides configuration file settings
  • Third-party library serializer being used

Solution: Check Java class annotations and confirm no other beans override ObjectMapper configuration.

Issue: Frontend receives numbers exceeding JavaScript safe integer range

Background: JavaScript’s Number.MAX_SAFE_INTEGER is 2^53 - 1 (approximately 9 quadrillion). Numbers exceeding this lose precision.

Solution: For large numbers (e.g., Twitter IDs, database BIGINT), serialize as strings:

public class TweetResponse {
    @JsonSerialize(using = ToStringSerializer.class)
    private Long tweetId;  // Exceeds JavaScript safe integer range, use string
}

Summary

To maintain consistent JSON response data types across different environments, we recommend unifying Jackson serialization configuration in your Spring Boot application. By modifying configuration files or customizing ObjectMapper, you can effectively resolve issues where numeric data serializes differently across environments. This ensures system stability and consistency.

Core recommendations:

  • Explicitly set WRITE_NUMBERS_AS_STRINGS=false (keep numeric types)
  • Explicitly set WRITE_DATES_AS_TIMESTAMPS=false (use ISO-8601 format)
  • Use identical configuration files across all environments
  • Write integration tests to verify serialization results
  • For numbers exceeding JavaScript safe integer range, use @JsonSerialize annotation to force string serialization

Related Articles

Leave a Comment