Don't seem to be able to edit the answer, but the double brackets were occurring because I was collecting the virtual nodes when I didn't need to. This is the new code:
MATCH (s1:Station)-[:TRAIN_SERVICE]->(s2:Station)-[:TRAIN_SERVICE]-(s3:Station)
CALL apoc.create.vRelationship(s1, 'ROUTE-EXISTS', {from:s1.stationCode, to:s3.stationCode}, s3) YIELD rel
WITH COLLECT(s1)+COLLECT(s3) AS stationsCollected, COLLECT(rel) AS vRoutes
WITH [station IN stationsCollected | apoc.create.vNode(['vStation'], {id: station.stationCode, data:{name: station.name}})] AS vStations, vRoutes
RETURN {nodes:vStations, edges:vRoutes}
resulting in the required response
{"nodes":[{"data":{"name":"Glasgow"},"id":"GLW"},{"data":{"name":"Edinburgh"},"id":"EDI"},{"data":{"name":"Penrith"},"id":"PNR"},
{"data":{"name":"York"},"id":"YRK"}],"edges":[{"from":"GLW","to":"PNR"},{"from":"EDI","to":"YRK"}]}