-
-
Notifications
You must be signed in to change notification settings - Fork 272
serialize before validate! #2573
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
|
|
@j-ibarra is attempting to deploy a commit to the Hey API Team on Vercel. A member of the Team first needs to authorize it. |
|
|
@j-ibarra how will your approach validate when I pass an object body such as {
foo: 1,
}that |
| @@ -1,2 +1,2 @@ | |||
| 'use strict';var A=async(t,r)=>{let e=typeof r=="function"?await r(t):r;if(e)return t.scheme==="bearer"?`Bearer ${e}`:t.scheme==="basic"?`Basic ${btoa(e)}`:e};var w=(t,r,e)=>{typeof e=="string"||e instanceof Blob?t.append(r,e):t.append(r,JSON.stringify(e));},P=(t,r,e)=>{typeof e=="string"?t.append(r,e):t.append(r,JSON.stringify(e));},_={bodySerializer:t=>{let r=new FormData;return Object.entries(t).forEach(([e,o])=>{o!=null&&(Array.isArray(o)?o.forEach(s=>w(r,e,s)):w(r,e,o));}),r}},x={bodySerializer:t=>JSON.stringify(t,(r,e)=>typeof e=="bigint"?e.toString():e)},U={bodySerializer:t=>{let r=new URLSearchParams;return Object.entries(t).forEach(([e,o])=>{o!=null&&(Array.isArray(o)?o.forEach(s=>P(r,e,s)):P(r,e,o));}),r.toString()}};var D=t=>{switch(t){case "label":return ".";case "matrix":return ";";case "simple":return ",";default:return "&"}},F=t=>{switch(t){case "form":return ",";case "pipeDelimited":return "|";case "spaceDelimited":return "%20";default:return ","}},M=t=>{switch(t){case "label":return ".";case "matrix":return ";";case "simple":return ",";default:return "&"}},O=({allowReserved:t,explode:r,name:e,style:o,value:s})=>{if(!r){let n=(t?s:s.map(c=>encodeURIComponent(c))).join(F(o));switch(o){case "label":return `.${n}`;case "matrix":return `;${e}=${n}`;case "simple":return n;default:return `${e}=${n}`}}let a=D(o),i=s.map(n=>o==="label"||o==="simple"?t?n:encodeURIComponent(n):m({allowReserved:t,name:e,value:n})).join(a);return o==="label"||o==="matrix"?a+i:i},m=({allowReserved:t,name:r,value:e})=>{if(e==null)return "";if(typeof e=="object")throw new Error("Deeply-nested arrays/objects aren\u2019t supported. Provide your own `querySerializer()` to handle these.");return `${r}=${t?e:encodeURIComponent(e)}`},R=({allowReserved:t,explode:r,name:e,style:o,value:s,valueOnly:a})=>{if(s instanceof Date)return a?s.toISOString():`${e}=${s.toISOString()}`;if(o!=="deepObject"&&!r){let c=[];Object.entries(s).forEach(([f,d])=>{c=[...c,f,t?d:encodeURIComponent(d)];});let p=c.join(",");switch(o){case "form":return `${e}=${p}`;case "label":return `.${p}`;case "matrix":return `;${e}=${p}`;default:return p}}let i=M(o),n=Object.entries(s).map(([c,p])=>m({allowReserved:t,name:o==="deepObject"?`${e}[${c}]`:c,value:p})).join(i);return o==="label"||o==="matrix"?i+n:n};var H=/\{[^{}]+\}/g,B=({path:t,url:r})=>{let e=r,o=r.match(H);if(o)for(let s of o){let a=false,i=s.substring(1,s.length-1),n="simple";i.endsWith("*")&&(a=true,i=i.substring(0,i.length-1)),i.startsWith(".")?(i=i.substring(1),n="label"):i.startsWith(";")&&(i=i.substring(1),n="matrix");let c=t[i];if(c==null)continue;if(Array.isArray(c)){e=e.replace(s,O({explode:a,name:i,style:n,value:c}));continue}if(typeof c=="object"){e=e.replace(s,R({explode:a,name:i,style:n,value:c,valueOnly:true}));continue}if(n==="matrix"){e=e.replace(s,`;${m({name:i,value:c})}`);continue}let p=encodeURIComponent(n==="label"?`.${c}`:c);e=e.replace(s,p);}return e},k=({allowReserved:t,array:r,object:e}={})=>s=>{let a=[];if(s&&typeof s=="object")for(let i in s){let n=s[i];if(n!=null)if(Array.isArray(n)){let c=O({allowReserved:t,explode:true,name:i,style:"form",value:n,...r});c&&a.push(c);}else if(typeof n=="object"){let c=R({allowReserved:t,explode:true,name:i,style:"deepObject",value:n,...e});c&&a.push(c);}else {let c=m({allowReserved:t,name:i,value:n});c&&a.push(c);}}return a.join("&")},I=t=>{if(!t)return "stream";let r=t.split(";")[0]?.trim();if(r){if(r.startsWith("application/json")||r.endsWith("+json"))return "json";if(r==="multipart/form-data")return "formData";if(["application/","audio/","image/","video/"].some(e=>r.startsWith(e)))return "blob";if(r.startsWith("text/"))return "text"}},W=(t,r)=>r?!!(t.headers.has(r)||t.query?.[r]||t.headers.get("Cookie")?.includes(`${r}=`)):false,T=async({security:t,...r})=>{for(let e of t){if(W(r,e.name))continue;let o=await A(e,r.auth);if(!o)continue;let s=e.name??"Authorization";switch(e.in){case "query":r.query||(r.query={}),r.query[s]=o;break;case "cookie":r.headers.append("Cookie",`${s}=${o}`);break;case "header":default:r.headers.set(s,o);break}}},q=t=>N({baseUrl:t.baseUrl,path:t.path,query:t.query,querySerializer:typeof t.querySerializer=="function"?t.querySerializer:k(t.querySerializer),url:t.url}),N=({baseUrl:t,path:r,query:e,querySerializer:o,url:s})=>{let a=s.startsWith("/")?s:`/${s}`,i=(t??"")+a;r&&(i=B({path:r,url:i}));let n=e?o(e):"";return n.startsWith("?")&&(n=n.substring(1)),n&&(i+=`?${n}`),i},z=(t,r)=>{let e={...t,...r};return e.baseUrl?.endsWith("/")&&(e.baseUrl=e.baseUrl.substring(0,e.baseUrl.length-1)),e.headers=C(t.headers,r.headers),e},C=(...t)=>{let r=new Headers;for(let e of t){if(!e||typeof e!="object")continue;let o=e instanceof Headers?e.entries():Object.entries(e);for(let[s,a]of o)if(a===null)r.delete(s);else if(Array.isArray(a))for(let i of a)r.append(s,i);else a!==void 0&&r.set(s,typeof a=="object"?JSON.stringify(a):a);}return r},g=class{_fns;constructor(){this._fns=[];}clear(){this._fns=[];}getInterceptorIndex(r){return typeof r=="number"?this._fns[r]?r:-1:this._fns.indexOf(r)}exists(r){let e=this.getInterceptorIndex(r);return !!this._fns[e]}eject(r){let e=this.getInterceptorIndex(r);this._fns[e]&&(this._fns[e]=null);}update(r,e){let o=this.getInterceptorIndex(r);return this._fns[o]?(this._fns[o]=e,r):false}use(r){return this._fns=[...this._fns,r],this._fns.length-1}},E=()=>({error:new g,request:new g,response:new g}),Q=k({allowReserved:false,array:{explode:true,style:"form"},object:{explode:true,style:"deepObject"}}),V={"Content-Type":"application/json"},j=(t={})=>({...x,headers:V,parseAs:"auto",querySerializer:Q,...t});var J=(t={})=>{let r=z(j(),t),e=()=>({...r}),o=i=>(r=z(r,i),e()),s=E(),a=async i=>{let n={...r,...i,fetch:i.fetch??r.fetch??globalThis.fetch,headers:C(r.headers,i.headers)};n.security&&await T({...n,security:n.security}),n.requestValidator&&await n.requestValidator(n),n.body&&n.bodySerializer&&(n.body=n.bodySerializer(n.body)),(n.body===void 0||n.body==="")&&n.headers.delete("Content-Type");let c=q(n),p={redirect:"follow",...n},f=new Request(c,p);for(let u of s.request._fns)u&&(f=await u(f,n));let d=n.fetch,l=await d(f);for(let u of s.response._fns)u&&(l=await u(l,f,n));let b={request:f,response:l};if(l.ok){if(l.status===204||l.headers.get("Content-Length")==="0")return {data:{},...b};let u=(n.parseAs==="auto"?I(l.headers.get("Content-Type")):n.parseAs)??"json",h;switch(u){case "arrayBuffer":case "blob":case "formData":case "json":case "text":h=await l[u]();break;case "stream":return {data:l.body,...b}}return u==="json"&&(n.responseValidator&&await n.responseValidator(h),n.responseTransformer&&(h=await n.responseTransformer(h))),{data:h,...b}}let S=await l.text();try{S=JSON.parse(S);}catch{}let y=S;for(let u of s.error._fns)u&&(y=await u(S,l,f,n));if(y=y||{},n.throwOnError)throw y;return {error:y,...b}};return {buildUrl:q,connect:i=>a({...i,method:"CONNECT"}),delete:i=>a({...i,method:"DELETE"}),get:i=>a({...i,method:"GET"}),getConfig:e,head:i=>a({...i,method:"HEAD"}),interceptors:s,options:i=>a({...i,method:"OPTIONS"}),patch:i=>a({...i,method:"PATCH"}),post:i=>a({...i,method:"POST"}),put:i=>a({...i,method:"PUT"}),request:a,setConfig:o,trace:i=>a({...i,method:"TRACE"})}};var K={$body_:"body",$headers_:"headers",$path_:"path",$query_:"query"},L=Object.entries(K),$=(t,r)=>{r||(r=new Map);for(let e of t)"in"in e?e.key&&r.set(e.key,{in:e.in,map:e.map}):e.args&&$(e.args,r);return r},G=t=>{for(let[r,e]of Object.entries(t))e&&typeof e=="object"&&!Object.keys(e).length&&delete t[r];},v=(t,r)=>{let e={body:{},headers:{},path:{},query:{}},o=$(r),s;for(let[a,i]of t.entries())if(r[a]&&(s=r[a]),!!s)if("in"in s)if(s.key){let n=o.get(s.key),c=n.map||s.key;e[n.in][c]=i;}else e.body=i;else for(let[n,c]of Object.entries(i??{})){let p=o.get(n);if(p){let f=p.map||n;e[p.in][f]=c;}else {let f=L.find(([d])=>n.startsWith(d));if(f){let[d,l]=f;e[l][n.slice(d.length)]=c;}else for(let[d,l]of Object.entries(s.allowExtra??{}))if(l){e[d][n]=c;break}}}return G(e),e};exports.buildClientParams=v;exports.createClient=J;exports.createConfig=j;exports.formDataBodySerializer=_;exports.jsonBodySerializer=x;exports.urlSearchParamsBodySerializer=U;//# sourceMappingURL=index.cjs.map | |||
| 'use strict';var A=async(t,r)=>{let e=typeof r=="function"?await r(t):r;if(e)return t.scheme==="bearer"?`Bearer ${e}`:t.scheme==="basic"?`Basic ${btoa(e)}`:e};var w=(t,r,e)=>{typeof e=="string"||e instanceof Blob?t.append(r,e):t.append(r,JSON.stringify(e));},P=(t,r,e)=>{typeof e=="string"?t.append(r,e):t.append(r,JSON.stringify(e));},_={bodySerializer:t=>{let r=new FormData;return Object.entries(t).forEach(([e,o])=>{o!=null&&(Array.isArray(o)?o.forEach(s=>w(r,e,s)):w(r,e,o));}),r}},x={bodySerializer:t=>JSON.stringify(t,(r,e)=>typeof e=="bigint"?e.toString():e)},U={bodySerializer:t=>{let r=new URLSearchParams;return Object.entries(t).forEach(([e,o])=>{o!=null&&(Array.isArray(o)?o.forEach(s=>P(r,e,s)):P(r,e,o));}),r.toString()}};var D=t=>{switch(t){case "label":return ".";case "matrix":return ";";case "simple":return ",";default:return "&"}},F=t=>{switch(t){case "form":return ",";case "pipeDelimited":return "|";case "spaceDelimited":return "%20";default:return ","}},M=t=>{switch(t){case "label":return ".";case "matrix":return ";";case "simple":return ",";default:return "&"}},O=({allowReserved:t,explode:r,name:e,style:o,value:s})=>{if(!r){let n=(t?s:s.map(c=>encodeURIComponent(c))).join(F(o));switch(o){case "label":return `.${n}`;case "matrix":return `;${e}=${n}`;case "simple":return n;default:return `${e}=${n}`}}let a=D(o),i=s.map(n=>o==="label"||o==="simple"?t?n:encodeURIComponent(n):m({allowReserved:t,name:e,value:n})).join(a);return o==="label"||o==="matrix"?a+i:i},m=({allowReserved:t,name:r,value:e})=>{if(e==null)return "";if(typeof e=="object")throw new Error("Deeply-nested arrays/objects aren\u2019t supported. Provide your own `querySerializer()` to handle these.");return `${r}=${t?e:encodeURIComponent(e)}`},R=({allowReserved:t,explode:r,name:e,style:o,value:s,valueOnly:a})=>{if(s instanceof Date)return a?s.toISOString():`${e}=${s.toISOString()}`;if(o!=="deepObject"&&!r){let c=[];Object.entries(s).forEach(([f,d])=>{c=[...c,f,t?d:encodeURIComponent(d)];});let p=c.join(",");switch(o){case "form":return `${e}=${p}`;case "label":return `.${p}`;case "matrix":return `;${e}=${p}`;default:return p}}let i=M(o),n=Object.entries(s).map(([c,p])=>m({allowReserved:t,name:o==="deepObject"?`${e}[${c}]`:c,value:p})).join(i);return o==="label"||o==="matrix"?i+n:n};var H=/\{[^{}]+\}/g,B=({path:t,url:r})=>{let e=r,o=r.match(H);if(o)for(let s of o){let a=false,i=s.substring(1,s.length-1),n="simple";i.endsWith("*")&&(a=true,i=i.substring(0,i.length-1)),i.startsWith(".")?(i=i.substring(1),n="label"):i.startsWith(";")&&(i=i.substring(1),n="matrix");let c=t[i];if(c==null)continue;if(Array.isArray(c)){e=e.replace(s,O({explode:a,name:i,style:n,value:c}));continue}if(typeof c=="object"){e=e.replace(s,R({explode:a,name:i,style:n,value:c,valueOnly:true}));continue}if(n==="matrix"){e=e.replace(s,`;${m({name:i,value:c})}`);continue}let p=encodeURIComponent(n==="label"?`.${c}`:c);e=e.replace(s,p);}return e},k=({allowReserved:t,array:r,object:e}={})=>s=>{let a=[];if(s&&typeof s=="object")for(let i in s){let n=s[i];if(n!=null)if(Array.isArray(n)){let c=O({allowReserved:t,explode:true,name:i,style:"form",value:n,...r});c&&a.push(c);}else if(typeof n=="object"){let c=R({allowReserved:t,explode:true,name:i,style:"deepObject",value:n,...e});c&&a.push(c);}else {let c=m({allowReserved:t,name:i,value:n});c&&a.push(c);}}return a.join("&")},I=t=>{if(!t)return "stream";let r=t.split(";")[0]?.trim();if(r){if(r.startsWith("application/json")||r.endsWith("+json"))return "json";if(r==="multipart/form-data")return "formData";if(["application/","audio/","image/","video/"].some(e=>r.startsWith(e)))return "blob";if(r.startsWith("text/"))return "text"}},W=(t,r)=>r?!!(t.headers.has(r)||t.query?.[r]||t.headers.get("Cookie")?.includes(`${r}=`)):false,T=async({security:t,...r})=>{for(let e of t){if(W(r,e.name))continue;let o=await A(e,r.auth);if(!o)continue;let s=e.name??"Authorization";switch(e.in){case "query":r.query||(r.query={}),r.query[s]=o;break;case "cookie":r.headers.append("Cookie",`${s}=${o}`);break;case "header":default:r.headers.set(s,o);break}}},q=t=>N({baseUrl:t.baseUrl,path:t.path,query:t.query,querySerializer:typeof t.querySerializer=="function"?t.querySerializer:k(t.querySerializer),url:t.url}),N=({baseUrl:t,path:r,query:e,querySerializer:o,url:s})=>{let a=s.startsWith("/")?s:`/${s}`,i=(t??"")+a;r&&(i=B({path:r,url:i}));let n=e?o(e):"";return n.startsWith("?")&&(n=n.substring(1)),n&&(i+=`?${n}`),i},z=(t,r)=>{let e={...t,...r};return e.baseUrl?.endsWith("/")&&(e.baseUrl=e.baseUrl.substring(0,e.baseUrl.length-1)),e.headers=C(t.headers,r.headers),e},C=(...t)=>{let r=new Headers;for(let e of t){if(!e||typeof e!="object")continue;let o=e instanceof Headers?e.entries():Object.entries(e);for(let[s,a]of o)if(a===null)r.delete(s);else if(Array.isArray(a))for(let i of a)r.append(s,i);else a!==void 0&&r.set(s,typeof a=="object"?JSON.stringify(a):a);}return r},g=class{_fns;constructor(){this._fns=[];}clear(){this._fns=[];}getInterceptorIndex(r){return typeof r=="number"?this._fns[r]?r:-1:this._fns.indexOf(r)}exists(r){let e=this.getInterceptorIndex(r);return !!this._fns[e]}eject(r){let e=this.getInterceptorIndex(r);this._fns[e]&&(this._fns[e]=null);}update(r,e){let o=this.getInterceptorIndex(r);return this._fns[o]?(this._fns[o]=e,r):false}use(r){return this._fns=[...this._fns,r],this._fns.length-1}},E=()=>({error:new g,request:new g,response:new g}),Q=k({allowReserved:false,array:{explode:true,style:"form"},object:{explode:true,style:"deepObject"}}),V={"Content-Type":"application/json"},j=(t={})=>({...x,headers:V,parseAs:"auto",querySerializer:Q,...t});var J=(t={})=>{let r=z(j(),t),e=()=>({...r}),o=i=>(r=z(r,i),e()),s=E(),a=async i=>{let n={...r,...i,fetch:i.fetch??r.fetch??globalThis.fetch,headers:C(r.headers,i.headers)};n.security&&await T({...n,security:n.security}),n.body&&n.bodySerializer&&(n.body=n.bodySerializer(n.body)),n.requestValidator&&await n.requestValidator(n),(n.body===void 0||n.body==="")&&n.headers.delete("Content-Type");let c=q(n),p={redirect:"follow",...n},f=new Request(c,p);for(let u of s.request._fns)u&&(f=await u(f,n));let d=n.fetch,l=await d(f);for(let u of s.response._fns)u&&(l=await u(l,f,n));let b={request:f,response:l};if(l.ok){if(l.status===204||l.headers.get("Content-Length")==="0")return {data:{},...b};let u=(n.parseAs==="auto"?I(l.headers.get("Content-Type")):n.parseAs)??"json",h;switch(u){case "arrayBuffer":case "blob":case "formData":case "json":case "text":h=await l[u]();break;case "stream":return {data:l.body,...b}}return u==="json"&&(n.responseValidator&&await n.responseValidator(h),n.responseTransformer&&(h=await n.responseTransformer(h))),{data:h,...b}}let S=await l.text();try{S=JSON.parse(S);}catch{}let y=S;for(let u of s.error._fns)u&&(y=await u(S,l,f,n));if(y=y||{},n.throwOnError)throw y;return {error:y,...b}};return {buildUrl:q,connect:i=>a({...i,method:"CONNECT"}),delete:i=>a({...i,method:"DELETE"}),get:i=>a({...i,method:"GET"}),getConfig:e,head:i=>a({...i,method:"HEAD"}),interceptors:s,options:i=>a({...i,method:"OPTIONS"}),patch:i=>a({...i,method:"PATCH"}),post:i=>a({...i,method:"POST"}),put:i=>a({...i,method:"PUT"}),request:a,setConfig:o,trace:i=>a({...i,method:"TRACE"})}};var K={$body_:"body",$headers_:"headers",$path_:"path",$query_:"query"},L=Object.entries(K),$=(t,r)=>{r||(r=new Map);for(let e of t)"in"in e?e.key&&r.set(e.key,{in:e.in,map:e.map}):e.args&&$(e.args,r);return r},G=t=>{for(let[r,e]of Object.entries(t))e&&typeof e=="object"&&!Object.keys(e).length&&delete t[r];},v=(t,r)=>{let e={body:{},headers:{},path:{},query:{}},o=$(r),s;for(let[a,i]of t.entries())if(r[a]&&(s=r[a]),!!s)if("in"in s)if(s.key){let n=o.get(s.key),c=n.map||s.key;e[n.in][c]=i;}else e.body=i;else for(let[n,c]of Object.entries(i??{})){let p=o.get(n);if(p){let f=p.map||n;e[p.in][f]=c;}else {let f=L.find(([d])=>n.startsWith(d));if(f){let[d,l]=f;e[l][n.slice(d.length)]=c;}else for(let[d,l]of Object.entries(s.allowExtra??{}))if(l){e[d][n]=c;break}}}return G(e),e};exports.buildClientParams=v;exports.createClient=J;exports.createConfig=j;exports.formDataBodySerializer=_;exports.jsonBodySerializer=x;exports.urlSearchParamsBodySerializer=U;//# sourceMappingURL=index.cjs.map | |||
Check notice
Code scanning / CodeQL
Semicolon insertion Note test
the enclosing function
commit: |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #2573 +/- ##
=======================================
Coverage 23.61% 23.61%
=======================================
Files 363 363
Lines 36504 36504
Branches 1562 1562
=======================================
Hits 8622 8622
Misses 27869 27869
Partials 13 13
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
In this case keep the same behavior When I declare type Decimal from the decimal lib serialize the type to string, example const value = {
food: new Decimal(100)
}or const value = {
food: new Decimal("100")
}the Zod generated is food: z.string()for this reason a thing that we need the parser before the validation using in the body paser
The result is const value = {
food: "100.00"
}The response flow is
the request flow should be
|
|
If your body serializer turns an object with an array into a serialized string body, how will you validate using Zod that the array has at least 3 entries? |
using JSON.parse(JSON.stringify(data))the object return the primitive type, arrays could be validate with zod normally other example using JSON.parse(JSON.stringify(data))class MyEncryptedDate {
private value: string;
constructor(encrypted:string) {
this.value = new Date(encryptionHelper(encrypted))
}
toJSON() {
return new Date(this.value);
}
}type dto = {
value: MyEncryptedDate,
values: MyEncryptedDate[]
}
const body = JSON.parse(JSON.stringify(data))The type of the body should be type dto = {
value: Date,
values: Date[]
} |
|
implement with local script #!/usr/bin/env tsx
/* eslint-disable no-console */
import { readFileSync, writeFileSync } from "fs";
import { dirname, resolve } from "path";
import { fileURLToPath } from "url";
/**
* Script to swap the order of bodySerializer and requestValidator in beforeRequest method
* Changes from:
* requestValidator -> bodySerializer
* To:
* bodySerializer -> requestValidator
*/
const __dirname = dirname(fileURLToPath(import.meta.url));
const files = [
resolve(__dirname, "./generated/client/client.gen.ts"),
resolve(__dirname, "./generated-legacy/client/client.gen.ts"),
];
function swapRequestOrder(content: string): string {
// Pattern to match the beforeRequest method with the incorrect order
const pattern =
/if \(opts\.requestValidator\) \{[\s\S]*?await opts\.requestValidator\(opts\);[\s\S]*?\}[\s\S]*?if \(opts\.body !== undefined && opts\.bodySerializer\) \{[\s\S]*?opts\.body = opts\.bodySerializer\(opts\.body\);[\s\S]*?\}/g;
// Replacement with the correct order
const replacement = `if (opts.body !== undefined && opts.bodySerializer) {
opts.body = opts.bodySerializer(opts.body);
}
if (opts.requestValidator) {
await opts.requestValidator(opts);
}`;
// Check if the pattern exists
if (pattern.test(content)) {
console.log(" ✓ Found pattern to replace");
return content.replace(pattern, replacement);
}
// Alternative: Look for just the specific lines and swap them
const lines = content.split("\n");
let modified = false;
for (let i = 0; i < lines.length - 5; i++) {
// Look for the requestValidator block followed by bodySerializer block
if (
lines[i].includes("if (opts.requestValidator)") &&
lines[i + 1].includes("await opts.requestValidator(opts)") &&
lines[i + 4].includes("if (opts.body !== undefined && opts.bodySerializer)") &&
lines[i + 5].includes("opts.body = opts.bodySerializer(opts.body)")
) {
// Swap the blocks
const requestValidatorBlock = [lines[i], lines[i + 1], lines[i + 2]];
const bodySerializerBlock = [lines[i + 4], lines[i + 5], lines[i + 6]];
// Replace with swapped order
lines[i] = bodySerializerBlock[0];
lines[i + 1] = bodySerializerBlock[1];
lines[i + 2] = bodySerializerBlock[2];
lines[i + 4] = requestValidatorBlock[0];
lines[i + 5] = requestValidatorBlock[1];
lines[i + 6] = requestValidatorBlock[2];
modified = true;
console.log(" ✓ Swapped order of requestValidator and bodySerializer");
}
}
if (modified) {
return lines.join("\n");
}
console.log(" ⚠ Pattern not found, file may already be fixed or has different structure");
return content;
}
function processFile(filePath: string): void {
console.log(`\nProcessing: ${filePath}`);
try {
// Read the file
const content = readFileSync(filePath, "utf-8");
console.log(" ✓ File read successfully");
// Apply the transformation
const modifiedContent = swapRequestOrder(content);
// Check if content was actually modified
if (content !== modifiedContent) {
// Write back to file
writeFileSync(filePath, modifiedContent, "utf-8");
console.log(" ✓ File updated successfully");
} else {
console.log(" ℹ No changes needed");
}
} catch (error) {
console.error(` ✗ Error processing file: ${error}`);
}
}
// Main execution
console.log("Starting to fix request order in client files...\n");
files.forEach(processFile);
console.log("\n✅ Script completed!"); |
Fix: Serialize request body before validation to support custom types
Problem
When using custom transformers (introduced in #2281 and #2383) with types like
Decimal, there was a type mismatch between the TypeScript interface and the OpenAPI schema validation:stringwith formatdecimalDecimal(generated via typeTransformers)Decimalinstance before serializationThis caused validation failures because the validator expected a
stringbut received aDecimalobject.Solution
Reordered the request processing pipeline to serialize before validate:
Before:
After:
Usage
With this fix, users can now use custom types with proper serialization:
This ensures compatibility between custom TypeScript types and OpenAPI schema validation while maintaining type safety throughout the request lifecycle.