NestJS: typeerror: do not know how to serialize a bigint
Postgres의 BIGSERIAL
타입을 Prisma로 가져오면 BigInt
타입이 됩니다. 이걸 #Nest.JS 컨트롤러에서 그대로 반환하면 이런 오류를 보게 되죠.
typeerror: do not know how to serialize a bigint
JSON.stringify()
함수에서 왠지 bigint
타입을 지원하지 않기 때문에 발생하는 문제라고 하는데, bigint
를 반환하기 전에 string
같은 걸로 바꿔 주면 간단히 해결되겠지만 그렇다고 해서 bigint
가 들어가는 모든 모델에 대해 타입 변환 함수 같은 걸 작성해 주기는 귀찮죠.
그런고로 오늘의 #뚝딱뚝딱 은 응답 DTO의 모든 bigint
를 알아서 string
으로 바꿔 주는 방법입니다. 더 나은 해결책이 있을 것 같기는 한데, 저는 이렇게 해결했어요.
Interceptor가 뭐예요?
인터셉터는 NestJS 애플리케이션에서 요청과 응답을 가로채거나 변형할 수 있도록 하는 클래스로, @Injectable()
데코레이터를 사용해야 하며 NestInterceptor
인터페이스를 구현해야 합니다.
NestInterceptor
인터페이스를 구현하려면 intercept(context: ExecutionContext, next: CallHandler)
함수를 구현해야 하는데, 우리는 CallHandler
만 쓸 거예요. CallHandler.handle()
을 써서 루트 핸들러를 호출할 수 있고, 그 결과를 pipe()
로 조작할 수 있습니다. 그러니까 우리는 루트 핸들러가 실행되어 나온 결과에 bigint
가 있으면 모조리 string
으로 바꿔 주는 인터셉터를 구현하면 되는 거죠.
ExecutionContext
에 대해서는 문서를 참조합시다.
bigint
잡기
그냥 객체의 모든 키를 재귀적으로 순회하면서 typeof data[key] === 'bigint'
면 data[key] = data[key].toString()
을 실행해 줍시다.
convertBigIntToString(data: any): any {
// 배열 타입이라면...
if (Array.isArray(data)) {
// 배열 안의 모든 요소에 대해 실행
return data.map((item) => this.convertBigIntToString(item));
} else if (data !== null && typeof data === 'object') {
// 오브젝트 안의 모든 필드에 대해...
Object.keys(data).forEach((key) => {
// 필드의 타입이 bigint라면...
if (typeof data[key] === 'bigint') {
// string으로 바꿔 주기
data[key] = data[key].toString();
// 필드의 타입이 object라면...
} else if (typeof data[key] === 'object') {
// nested object에 대해 같은 작업 반복
convertBigIntToString(data[key]);
}
});
}
return data;
}
인터셉터 구현하기
next.handle().pipe()
에서 pipe()
에 들어가는 인자는 rxjs.OperatorFunction
인데, rxjs
의 Observable
을 인자로 받는 함수입니다. 여기서 Observable
로는 출력 스트림이 들어갑니다. 자세한 설명은 접어두고, 우리는 핸들러의 결과에 convertBigIntToString()
을 실행하면 되니까 rxjs/operators
에서 map
함수를 가져옵시다.
import {
CallHandler,
ExecutionContext,
Injectable,
NestInterceptor,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class BigIntInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(map((data) => this.convertBigIntToString(data)));
}
private convertBigIntToString(data: any): any {
if (Array.isArray(data)) {
return data.map((item) => this.convertBigIntToString(item));
} else if (data !== null && typeof data === 'object') {
Object.keys(data).forEach((key) => {
if (typeof data[key] === 'bigint') {
data[key] = data[key].toString();
} else if (typeof data[key] === 'object') {
this.convertBigIntToString(data[key]);
}
});
}
return data;
}
}
인터셉터 적용하기
@UseInterceptors()
데코레이터를 사용해서 컨트롤러나 메소드에 Interceptor를 적용할 수 있습니다.
// hello.dto.ts
// bigint가 request에 있는 경우 ValidationPipe({transform: true})를 사용하기만 해도 됩니다.
export class HelloRequestDto {
@ApiProperty()
hello: bigint;
}
// bigint가 response에 있는 경우 JSON.serialize()에서 bigint를 처리하지 못하므로 인터셉터가 필요합니다.
export class HelloResponseDto {
@ApiProperty()
goodbye: bigint;
}
// hello.controller.ts
@Get('hello')
@UseInterceptors(BigIntInterceptor)
helloController(
@Query() body: HelloRequestDto
): HelloResponseDto {
return ({ goodbye: body.hello })
}
app.useGlobalInterceptors()
를 쓰면 전역 범위에 적용할 수도 있습니다.
// main.ts
async function bootstrap() {
/* ... */
app.useGlobalInterceptors(
new BigIntInterceptor(),
);
/* ... */
}
이제 bigint
가 포함된 요청을 전송하면 결과가 string
으로 변환되어 날아오는 것을 확인할 수 있습니다.
Dani Soohan Park (@heartade)
Follow this blog at Fediverse: @heartade@blog.heartade.dev
Follow my shorter shoutouts at Fediverse: @heartade@social.silicon.moe
Follow me at Bluesky: @heartade.dev