Here’s the whole approach as one flow:
#!/bin/bash
SERVER=...
CATALOG=...
ORG=...
PRODUCT=...
# 1. Pull the raw output (binary zip blob + JSON, concatenated)
apic products:get --scope catalog -s $SERVER -c $CATALOG -o $ORG \
--format json --output - $PRODUCT > raw.bin
# 2. Find the byte offset of the LAST NUL byte in the file.
# Zip data is full of 0x00 bytes; valid JSON text never contains them,
# so this marks the end of the binary blob.
LAST_NUL=$(grep -aboP '\x00' raw.bin | tail -1 | cut -d: -f1)
# 3. Take everything after that...
tail -c +$((LAST_NUL + 2)) raw.bin > tail.bin
# 4. ...then find the first '{' in what remains. That's the true start of the JSON.
FIRST_BRACE=$(grep -abo '{' tail.bin | head -1 | cut -d: -f1)
tail -c +$((FIRST_BRACE + 1)) tail.bin > clean.json
# 5. Now parse normally
jq '{product: .name, version: .version, apis: [.apis[] | {name: .name, version: .version}]}' clean.json
This works regardless of which API in the product is the one carrying the WSDL/XSD zip, the NUL-byte boundary just strips off whatever binary precedes the JSON, leaving you with the same clean document structure your current jq parsing already expects.
One thing to verify on a real sample: confirm there’s only one binary blob per product output (i.e., one API has the zip attachment, but the JSON metadata for the whole product, including all contained APIs and versions comes as a single trailing block). If that’s the case, this script is a drop-in replacement so run it for every product, and for products without any embedded zip, LAST_NUL will simply come back empty/0 and the script degrades gracefully to just passing the whole file through (you may want to add a check: if [ -z "$LAST_NUL" ]; then cp raw.bin clean.json; fi).
If instead each contained API gets its own binary+JSON segment back-to-back, you’d need to split on every NUL-run-to-{ transition and jq each segment separately, then merge results.