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

Jackson配置问题:密码仅在DTO转Entity传递,不返回前端

问题原因分析

你遇到的问题核心是@JsonProperty(access = Access.WRITE_ONLY)的作用逻辑:这个注解限制字段只能被反序列化(JSON转对象),不能被序列化(对象转JSON)。而你用mapper.convertValue(userDto, User.class)时,本质是先把UserDto序列化成Jackson的中间数据结构,再反序列化成User对象——这一步需要读取UserDto的password字段(属于序列化操作),但WRITE_ONLY规则阻止了这个读取行为,导致密码无法传递到User实体中。

下面提供三种可行的解决方案,按推荐优先级排序:


方案1:拆分请求/响应DTO(推荐)

遵循单一职责原则,创建两个独立的DTO分别处理请求接收和响应返回,彻底避免字段冲突:

定义新的DTO

// 仅用于接收创建用户的请求
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CreateUserRequest {
    private String username;
    private String password; // 无需加WRITE_ONLY,因为仅作为请求体使用
}

// 仅用于返回用户响应
@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UserResponse {
    private String username;
    private Date createDate;
}

修改Controller代码

public class UserController {
    @Autowired
    private ObjectMapper objectMapper;

    @PostMapping("/user")
    public UserResponse addUser(CreateUserRequest request) {
        // 请求DTO转User实体
        User user = objectMapper.convertValue(request, User.class);
        User savedUser = userService.add(user);
        // 保存后的实体转响应DTO
        return objectMapper.convertValue(savedUser, UserResponse.class);
    }
}

这种方式逻辑清晰,后续扩展字段时不会相互影响,是最规范的做法。


方案2:使用Jackson的@JsonView

通过视图控制不同场景下的字段可见性,无需拆分DTO:

定义视图接口

public class Views {
    // 用于写入场景:接收请求、DTO转实体
    public interface Write {}
    // 用于读取场景:返回响应
    public interface Read {}
}

修改UserDto

@AllArgsConstructor
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
@NoArgsConstructor
public class UserDto {
    private String username;
    
    // 仅在Write视图下可见(可被读取用于转实体),Read视图下隐藏
    @JsonView(Views.Write.class)
    private String password;
    
    // 仅在Read视图下可见(响应返回)
    @JsonView(Views.Read.class)
    private Date createDate;
}

修改Controller

public class UserController {
    @Autowired
    private ObjectMapper objectMapper;

    @PostMapping("/user")
    @JsonView(Views.Read.class) // 响应时仅返回Read视图包含的字段
    public UserDto addUser(@JsonView(Views.Write.class) UserDto userDto) {
        // 此时userDto包含password,可正常转实体
        User user = objectMapper.convertValue(userDto, User.class);
        User savedUser = userService.add(user);
        // 转回DTO后,password会被Read视图忽略
        return objectMapper.convertValue(savedUser, UserDto.class);
    }
}

注意:必须使用Spring注入的ObjectMapper,而非手动new的实例,才能正常识别@JsonView注解。


方案3:手动字段映射/使用MapStruct

如果不想调整DTO结构,可通过手动赋值或映射工具控制字段传递:

手动映射示例

public class UserController {
    @PostMapping("/user")
    public UserDto addUser(UserDto userDto) {
        // 手动将DTO字段赋值给实体,不受Jackson注解限制
        User user = new User();
        user.setUsername(userDto.getUsername());
        user.setPassword(userDto.getPassword());
        
        User savedUser = userService.add(user);
        
        // 手动构造响应DTO,不设置password
        UserDto responseDto = new UserDto();
        responseDto.setUsername(savedUser.getUsername());
        responseDto.setCreateDate(savedUser.getCreateDate());
        return responseDto;
    }
}

MapStruct映射示例

先添加MapStruct依赖,然后定义映射接口:

@Mapper(componentModel = "spring")
public interface UserMapper {
    UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
    
    // DTO转实体,自动映射所有字段(包括password)
    User toEntity(UserDto userDto);
    
    // 实体转DTO,忽略password字段
    @Mapping(target = "password", ignore = true)
    UserDto toDto(User user);
}

Controller中使用:

public class UserController {
    @Autowired
    private UserMapper userMapper;

    @PostMapping("/user")
    public UserDto addUser(UserDto userDto) {
        User user = userMapper.toEntity(userDto);
        User savedUser = userService.add(user);
        return userMapper.toDto(savedUser);
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 13:20:38