It looks like you need to do a dynamic JSON_VALUE lookup based on the cross-join of X and Y properties.
Note that dynamic JSON paths only work in SQL Servre 2017 onwards, not 2016.
DECLARE @json nvarchar(max) = '{"X":["x1","x2","x3"],"Y":["y1","y2","y3"],"Z":[["x1y1","x1y2","x1y3"],["x2y1","x2y2","x2y3"],["x3y1","x3y2","x3y3"],["x4y1","x4y2","x4y3"],["x5y1","x5y2","x5y3"]]}';
SELECT
x = x.value,
y = y.value,
z = JSON_VALUE(@json, '$.Z[' + x.[key] + '][' + y.[key] + ']')
FROM OPENJSON(@json, '$.X') x
CROSS JOIN OPENJSON(@json, '$.Y') y;
For SQL Server 2016, you instead just need to cross-join everything and filter afterwards
DECLARE @json nvarchar(max) = '{"X":["x1","x2","x3"],"Y":["y1","y2","y3"],"Z":[["x1y1","x1y2","x1y3"],["x2y1","x2y2","x2y3"],["x3y1","x3y2","x3y3"],["x4y1","x4y2","x4y3"],["x5y1","x5y2","x5y3"]]}';
SELECT
x = x.value,
y = y.value,
z = z2.value
FROM OPENJSON(@json, '$.X') x
CROSS JOIN OPENJSON(@json, '$.Y') y
JOIN OPENJSON(@json, '$.Z') z1 ON z1.[key] = x.[key]
CROSS APPLY OPENJSON(z1.value) z2
WHERE z2.[key] = y.[key];
db<>fiddle
Zseems to be a nested array. Is this expected? The json contains a list of column values instead of rows too. Frankly, the easiest way would be to use a Python script withsp_execute_external_script, load this into a DataFrame, return it and write the output. JSON functions are not meant for reshaping data