You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

序列化后属性命名异常:dialCode变dial_code的原因及处理方案?

TypeScript字段序列化不一致问题分析

问题场景

我定义了两个TypeScript接口:

interface Country {
  name: string,
  flag: string,
  code: string,
  dialCode: number
}

interface RegisterRequest {
  email: string,
  name: string,
  surname: string,
  password: string,
  rememberMe: boolean,
  country: Country
}

声明字段并发送请求:

protected form = {} as RegisterRequest;

this.http.post('/auth/register', this.form).subscribe(...);

最终请求体如下:

{
  "name": "John",
  "surname": "Doe",
  "country": {
    "name": "Burundi",
    "flag": "🇧🇮",
    "code": "BI",
    "dial_code": 257
  },
  "email": "john.doe@example.org",
  "password": "password",
  "rememberMe": true
}

可以看到dialCode被序列化为dial_code,但rememberMe保持原命名不变,后端Spring Boot需要手动用@JsonProperty("dial_code")映射字段。


现象原因

  1. dialCode转成dial_code的原因:你使用的国家选择组件(比如各类UI库的下拉组件)返回的Country对象本身就是下划线命名的dial_code,而TypeScript的as RegisterRequest只是类型断言——只告诉TS这个对象符合接口结构,不会修改实际对象的字段名,所以序列化后保留了组件返回的原始字段名。
  2. rememberMe不变的原因:这个字段是你自己手动赋值的,严格遵循了接口的驼峰命名,所以序列化后保持原样。

最优处理方式

方式一:前端统一转换(推荐,前后端规范对齐)

确保所有请求体字段符合后端的下划线命名规范,可通过两种方式实现:

1. 手动转换字段(单次请求)

写一个工具函数递归把驼峰转下划线,发送请求前转换:

// 驼峰转下划线工具函数
const toSnakeCase = (obj: any): any => {
  if (typeof obj !== 'object' || obj === null) return obj;
  if (Array.isArray(obj)) return obj.map(item => toSnakeCase(item));
  return Object.keys(obj).reduce((acc, key) => {
    const snakeKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
    acc[snakeKey] = toSnakeCase(obj[key]);
    return acc;
  }, {} as any);
};

// 发送请求时转换
this.http.post('/auth/register', toSnakeCase(this.form)).subscribe(...);

2. HTTP拦截器全局转换(多请求场景)

写一个Angular拦截器,自动处理所有POST/PUT请求的请求体:

import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable()
export class SnakeCaseInterceptor implements HttpInterceptor {
  intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> {
    if (request.body && typeof request.body === 'object') {
      const snakeCaseBody = this.toSnakeCase(request.body);
      request = request.clone({ body: snakeCaseBody });
    }
    return next.handle(request);
  }

  private toSnakeCase(obj: any): any {
    if (typeof obj !== 'object' || obj === null) return obj;
    if (Array.isArray(obj)) return obj.map(this.toSnakeCase.bind(this));
    return Object.keys(obj).reduce((acc, key) => {
      const snakeKey = key.replace(/([A-Z])/g, '_$1').toLowerCase();
      acc[snakeKey] = this.toSnakeCase(obj[key]);
      return acc;
    }, {} as any);
  }
}

然后在AppModule的providers数组里注册拦截器:

providers: [
  { provide: HTTP_INTERCEPTORS, useClass: SnakeCaseInterceptor, multi: true }
]

方式二:后端全局配置(前端无需修改)

在Spring Boot中配置Jackson全局命名策略,自动处理驼峰与下划线的映射:

import com.fasterxml.jackson.databind.PropertyNamingStrategies;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;

@Configuration
public class JacksonConfig {
    @Bean
    public Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder() {
        return new Jackson2ObjectMapperBuilder()
                .propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
    }
}

配置后,Jackson会自动将请求体的驼峰字段转成下划线,同时把响应体的下划线字段转成驼峰返回给前端,无需再手动加@JsonProperty。


操作误区说明

你的代码没有语法错误,但存在类型断言的认知误区:{} as RegisterRequest只是TypeScript的类型层面声明,不会修改实际对象的字段结构。当你从组件拿到的Country对象是dial_code时,类型断言只是让TS“假装”它符合dialCode的接口定义,实际字段名并没有改变,这才导致了序列化后的不一致。


内容的提问来源于stack exchange,提问作者ormai

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 16:25:54