-
-
Notifications
You must be signed in to change notification settings - Fork 236
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
base: main
Are you sure you want to change the base?
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[]
} |
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:string
with formatdecimal
Decimal
(generated via typeTransformers)Decimal
instance before serializationThis caused validation failures because the validator expected a
string
but received aDecimal
object.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.