본문 바로가기
빌드노트 검색

빌드노트

PowerShell에서 JSON 배열의 원소가 하나면 대괄호가 사라지는 이유

PowerShell 7에서 JSON 배열을 파이프로 왕복 변환하면 빈 배열은 출력이 없어지고 한 원소 배열은 객체가 될 수 있다. NoEnumerate와 직접 InputObject 전달을 비교하고, 변수를 다시 파이프로 보낼 때 생기는 열거와 버전 차이를 확인한다.

PowerShell에서 [{"id":1}]을 ConvertFrom-Json으로 읽고 ConvertTo-Json으로 되돌렸는데 {"id":1}이 나올 수 있다. 이 경로에서는 최상위 배열이 파이프라인에서 원소별로 전달돼 배열이라는 경계를 잃는다. 지원하는 환경에서는 ConvertFrom-Json -NoEnumerate로 배열을 하나의 객체로 넘긴다. 변수에 보관했다가 직렬화한다면 ConvertTo-Json -InputObject $items로 배열 자체를 전달해야 한다.

아래 비교는 Windows의 PowerShell 7.6.6에서 새 pwsh -NoProfile 자식을 실행한 결과다. 기존 JSON 파일 대신 합성 문자열만 사용했다. 자기 환경에서도 먼저 버전과 옵션 지원 여부를 확인한다. PowerShell 7이 설치돼 있다면 터미널에서 pwsh -NoProfile을 연 뒤 다음 두 줄을 실행한다.

$PSVersionTable.PSVersion.ToString()
(Get-Command ConvertFrom-Json).Parameters.ContainsKey('NoEnumerate')

실험에서는 순서대로 7.6.6, True가 나왔다. 별도로 실행한 Windows PowerShell 5.1.26100.9444에서는 두 번째 결과가 False였고, -NoEnumerate를 지정하면 매개변수를 찾지 못하는 오류가 났다. 5.1 공식 구문 (새 탭에서 열림)에도 이 옵션이 없다. False인 환경에 아래 옵션을 그대로 붙이지 말고, 실제 실행 셸과 그 버전의 동작부터 확인한다. 이하의 출력과 수정 예는 PowerShell 7.6.6을 기준으로 한다.

한 원소일 때 드러나는 구조 변화

다음 세 줄은 같은 입력을 기본 변환과 배열 열거를 막은 변환에 각각 통과시킨다. 파일이나 추가 패키지는 필요 없다.

$json = '[{"id":1}]'
$json | ConvertFrom-Json | ConvertTo-Json -Compress
$json | ConvertFrom-Json -NoEnumerate | ConvertTo-Json -Compress

첫 출력은 {"id":1}, 두 번째는 [{"id":1}]이었다. ConvertFrom-Json 문서의 NoEnumerate 설명 (새 탭에서 열림)은 배열 원소들을 따로 내보내는 대신 배열을 하나의 객체로 내보낸다고 설명한다. -Compress는 출력 공백을 줄이는 옵션이므로 대괄호가 사라진 원인이 아니다. ConvertTo-Json 문서 (새 탭에서 열림)에서도 이 용도를 확인할 수 있다.

첫 줄의 문자열을 아래 입력으로 바꿔 같은 두 변환을 실행했다. 출력 문자열을 별도 JSON 파서로 읽어 최상위 타입과 값도 비교했다.

  • []: 기본 경로는 출력 문자열 자체가 없었다. -NoEnumerate 경로는 []를 반환했다. 출력 없음과 JSON 문자열 null은 다르다.
  • [{"id":1}]: 기본 경로는 객체 {"id":1}, -NoEnumerate 경로는 원소 하나인 배열 [{"id":1}]이었다.
  • [{"id":1},{"id":2}]: 두 경로 모두 배열 [{"id":1},{"id":2}]이었다.

기본 ConvertFrom-Json이 내보낸 파이프라인 객체 수는 각각 0개·1개·2개였고, -NoEnumerate에서는 모두 1개였다. 기본 경로에서 ConvertTo-Json은 한 객체를 받으면 그 객체를 직렬화하고, 두 객체를 받으면 모아 배열로 직렬화했다. 그래서 원소가 두 개일 때 정상처럼 보여도 한 개나 빈 배열에서 구조가 달라진다. 배열 테스트에 두 원소짜리 입력만 쓰면 이 차이를 놓친다.

배열을 보관했어도 다시 파이프로 보내면 풀린다

-NoEnumerate는 해당 명령의 출력 방식을 바꾼다. 변수에 든 배열이 이후 모든 파이프라인에서도 하나의 객체로 취급된다는 뜻은 아니다. 다음 코드는 배열 보관 여부와 직렬화할 때의 전달 방식을 함께 확인한다.

$json = '[{"id":1}]'
$items = ConvertFrom-Json -InputObject $json -NoEnumerate
$items -is [array]
$items | ConvertTo-Json -Compress
ConvertTo-Json -InputObject $items -Compress

출력은 순서대로 True, {"id":1}, [{"id":1}]이었다. 변수에는 실제 배열이 있었지만 $items | ...에서 원소가 열거됐다. -InputObject $items는 그 배열 자체를 인자로 전달해 보존했다. 빈 배열도 같은 방식으로 비교했으며, 파이프라인은 출력 없음, 직접 인자는 []였다. 이 차이는 PowerShell 파이프라인의 원소별 전달 설명 (새 탭에서 열림)과 일치한다.

따라서 기존 스크립트에서는 JSON을 읽는 줄뿐 아니라 마지막으로 ConvertTo-Json에 넘기는 줄도 확인한다. 중간 변수에 배열을 보존했다면 마지막 줄을 직접 인자 방식으로 바꾼다. 반대로 기본 ConvertFrom-Json 결과를 이미 받았다면 -InputObject만 붙여서는 잃어버린 경계를 복구할 수 없다. 별도 비교에서 ConvertTo-Json -InputObject (ConvertFrom-Json -InputObject $json) -Compress는 한 원소 입력을 여전히 객체로 만들었고, 빈 배열 입력은 null로 만들었다.

입력 계약이 항상 배열이라면 명시적으로 모을 수 있다

PowerShell 7.6.6에서 기본 변환이 내보내는 원소들을 @(...)로 모으고, 그 배열을 직접 인자로 보내는 방법도 비교했다. 다음 코드는 입력의 최상위가 항상 배열이라는 계약이 있을 때 사용할 수 있다.

$json = '[{"id":1}]'
$items = @(ConvertFrom-Json -InputObject $json)
ConvertTo-Json -InputObject $items -Compress

이 입력의 출력은 [{"id":1}]이었다. []와 두 원소 입력으로 바꿔도 각각 [], [{"id":1},{"id":2}]를 유지했다. 이 방법에서도 마지막 줄을 $items | ConvertTo-Json으로 바꾸면 다시 열거된다.

다만 입력이 객체 {"id":1}일 때도 위 코드는 배열 [{"id":1}]로 감싼다. 배열인지 객체인지 원래 구조를 유지해야 한다면 -NoEnumerate로 읽고 배열을 직접 전달하는 쪽을 선택한다. Windows PowerShell 5.1에서는 같은 식으로 앞의 세 배열의 원래 구조를 유지하지 못했다. 5.1용 호환 대체식으로 사용하지 않는다.

ConvertTo-Json -AsArray만 덧붙이는 방법도 원래 구조 보존과는 구별해야 한다. AsArray 설명 (새 탭에서 열림)처럼 단일 객체도 배열로 감싼다. 실제 기본 파이프라인 뒤에 붙였을 때 한 원소 입력은 배열이 됐지만, 빈 배열은 여전히 출력이 없었고 원래 객체 입력까지 배열이 됐다. 빈 배열 처리와 객체·배열 구분이 필요한 왕복 변환을 이 옵션 하나로 해결했다고 판단하면 안 된다.

이번에 확인한 범위는 최상위의 빈 배열·한 원소·두 원소 배열과 몇 가지 경계 입력이다. 객체 속성에 들어 있는 {"items":[{"id":1}]}는 기본 왕복에서도 내부 배열을 유지했다. 반면 최상위가 배열인 [[1,2]]는 기본 경로에서 [1,2]가 됐고, -NoEnumerate를 바로 연결하거나 보존한 배열을 직접 인자로 전달하면 [[1,2]]를 유지했다. 이 결과를 모든 중첩 배열이 재귀적으로 풀린다는 규칙으로 확대하지 않는다.

배열 경계 보존과 직렬화 깊이도 별개다. 별도 경계 실험은 ConvertTo-Json -Depth 10을 사용했지만 본문의 짧은 입력은 기본 깊이에서도 같은 결과를 냈다. 날짜·큰 숫자·사용자 정의 객체·깊은 중첩 구조의 값 보존, 다른 PowerShell 7 버전과 다른 운영체제는 확인하지 않았다. 자기 데이터에서는 최종 JSON의 배열 여부와 원소 수를 확인하고, 빈 배열과 한 원소 배열도 함께 비교한다.

← 전체 글 보기