PowerShell JSON에 @{…}가 들어갈 때: ConvertTo-Json 깊이 문제와 확인 코드

JSON 파일은 만들어졌다. 에러도 없었다. 그런데 다시 읽어 보니 숫자가 있어야 할 자리에 @{attempts=3; delayMs=250}라는 문자열이 들어 있다. 저장은 됐지만, 저장하려던 구조는 남지 않은 것이다.

PowerShell에서 중첩된 설정이나 API 요청 본문을 만들 때 이런 증상이 나오면 ConvertTo-Json-Depth부터 확인하자. 공백을 줄이는 -Compress로는 해결되지 않았다. 아래는 Windows PowerShell 5.1과 PowerShell 7에서 같은 예제를 돌린 결과다.

문법 오류가 아니라 객체가 문자열이 된 문제

ConvertTo-Json의 기본 깊이는 2다. 이번 예제는 그보다 안쪽에 객체를 하나 더 넣었다. 충분한 깊이를 지정하지 않으면 그 객체의 attemptsdelayMs를 별도 필드로 읽을 수 없게 된다. 기본값은 Microsoft의 ConvertTo-Json 문서에서도 확인할 수 있다.

원래 데이터
settings → retry → policy → attempts: 3

기본 변환
policy = "@{attempts=3; delayMs=250}"
문자열 하나가 됨

깊이 지정 후
policy = {"attempts":3,"delayMs":250}
안쪽 필드를 가진 객체가 유지됨

같은 예제의 실제 출력에서 policy 부분만 발췌했다. 두 결과 모두 JSON으로 읽을 수 있다는 점이 함정이다.

같은 데이터로 4가지 변환을 비교했다

2026년 9월 10일, Windows의 powershell.exe 5.1.26100.9444와 pwsh.exe 7.6.5에서 각각 실행했다. 프로필을 불러오지 않는 별도 프로세스를 사용했다. 실제 계정·설정 파일·API 서버는 건드리지 않고 아래 테스트 객체만 메모리에서 변환했다.

아래 코드는 파일을 쓰거나 네트워크로 보내지 않는다. 먼저 PowerShell에 넣어 원본 객체를 만든다.

$payload = [pscustomobject]@{
    job = 'demo'
    settings = [pscustomobject]@{
        retry = [pscustomobject]@{
            policy = [pscustomobject]@{
                attempts = 3
                delayMs = 250
            }
        }
    }
}

$bad = $payload | ConvertTo-Json
$bad
($bad | ConvertFrom-Json).settings.retry.policy.GetType().Name

마지막 줄의 출력은 두 버전 모두 String이었다. ConvertFrom-Json이 실패하지 않았는데도 원래의 객체는 사라져 있었다.

변환 옵션5.1 결과7.6.5 결과
기본값문자열로 변경
경고 없음
문자열로 변경
깊이 경고
-Compress문자열로 변경
경고 없음
문자열로 변경
깊이 경고
-Depth 6객체·값 유지객체·값 유지
-Depth 6 -Compress객체·값 유지객체·값 유지

8가지 실행 모두 결과를 JSON으로 다시 읽는 데는 성공했다. 차이가 난 것은 policy가 객체인지, 그 안의 두 값이 남아 있는지였다. -Compress는 출력 모양을 줄이는 옵션이지 더 깊은 데이터를 보존하는 옵션이 아니다.

수정은 한 줄, 확인은 한 번 더

위에서 만든 $payload를 그대로 두고 이어서 실행한다. 이 코드도 파일을 덮어쓰지 않는다. 마지막에 출력되는 PAYLOAD_OK는 아래 검사를 통과했다는 뜻으로, 이 예제에서 직접 정한 표시다.

$json = $payload | ConvertTo-Json -Depth 6 -Compress
$check = $json | ConvertFrom-Json
$policy = $check.settings.retry.policy

if ($policy -isnot [pscustomobject] -or
    $policy.attempts -ne 3 -or
    $policy.delayMs -ne 250) {
    throw 'Payload structure changed'
}

$json
'PAYLOAD_OK'

이번 구조에는 6이면 충분했다. 모든 데이터에 맞는 숫자라는 뜻은 아니다. 서비스마다 필요한 중첩 구조가 다르므로 실제로 보낼 필드만 담은 객체를 만들고, 그 구조에 맞는 깊이와 검사 항목을 정하는 편이 낫다. 전체 설정이나 자격정보를 덤프해서 깊이만 크게 늘리지 말자.

이미 문자열로 바뀐 결과에 나중에 -Depth를 크게 붙여도 원래 필드가 자동 복구되지는 않는다. 수정 후에는 변환 전 원본 객체에서 JSON을 다시 만들어야 한다. 중요한 필드가 더 있다면 위 검사에도 추가한다. 이 세 조건이 전체 데이터의 동일성까지 증명하는 것은 아니다.

5.1에서는 경고가 없었다

7.6.5에서 기본 변환을 하면 깊이가 2를 넘었다는 경고가 나온다. Microsoft 문서는 이 경고가 PowerShell 7.1부터 추가됐다고 설명한다. 이번 5.1 실행에서는 같은 입력이 문자열로 바뀌어도 경고가 나오지 않았다.

7.6.5에서는 -WarningAction Stop을 붙인 기본 변환이 중단됐다. 5.1에서는 중단되지 않았다. 경고를 오류처럼 취급하는 방법만으로 두 환경을 모두 보호할 수는 없었다. 깊이를 명시하고 변환 후 값도 확인하는 이유다.

어느 PowerShell에서 실행됐는지 모르겠다면 $PSVersionTable.PSVersion부터 본다. 터미널 창의 모양이 같아도 실행 파일은 다를 수 있다.

내 증상이 이 문제에 해당할까

  • 원래 객체가 있던 곳에 @{...} 같은 문자열이 들어 있다: 변환 직전 원본과 -Depth를 비교한다. 원래부터 문자열인 필드를 오류로 오인하지는 말자.
  • JSON은 읽히지만 안쪽 필드를 못 찾는다: 가장 깊은 필드의 타입과 값을 함께 확인한다. 괄호가 맞는지만 검사하면 이번 오류를 놓친다.
  • 한글만 깨진다: 이 실험과 다른 증상이다. 파일을 쓰고 읽을 때의 인코딩을 별도로 확인해야 한다.
  • 파일 자체를 못 찾는다: 데이터 변환보다 앞 단계다. 시작 위치와 PowerShell 경로 점검 글에서 구분할 수 있다.

자동화가 초록색으로 끝났다고 결과까지 맞는 것은 아니었다. JSON을 만들었다는 로그 옆에, 중요한 필드가 그대로 남았다는 검사 하나를 더 두는 쪽이 뒤탈이 적다.


재현 범위와 출처
2026-09-10 Windows, Windows PowerShell 5.1.26100.9444 / PowerShell 7.6.5, NoProfile·NonInteractive 별도 실행. 중첩 PSCustomObject와 숫자 두 필드의 보존 여부를 비교했다. 실제 API 전송, 파일 인코딩, 순환 참조, 모든 .NET 타입의 왕복 보존은 시험하지 않았다. 공식 자료: ConvertTo-Json 7.6 · Windows PowerShell 5.1 문서. 5.1 페이지에도 경고 관련 표현이 있으나, 버전별 경고 여부는 7.1 도입 설명과 이번 실행 결과를 구분해 적었다.

Similar Posts