import 'reflect-metadata' ; const apiMap = 'api:map:' ; const apiMapSerializable = `${ apiMap }serializable` ; const designType = 'design:type' ; const designParamTypes = 'design:paramtypes' ; enum Type { Array = 'array' , Boolean = 'boolean' , Date = 'date' , Number = 'number' , Object = 'object' , String = 'string' } interface SerializableMetadata { baseClassNames : Array < string > ; options ? : SerializableOptions ; } interface SerializableOptions { formatPropertyNames : FormatPropertyNameProto ; } type IOProto = ( property : any , currentInstance ? : any ) => any ; type PredicateProto = ( property : any , parentProperty ? : any ) => any ; type FormatPropertyNameProto = ( propertyName : string ) => string ; interface BeforeAfterProto { beforeSerialize ? : IOProto ; afterSerialize ? : IOProto ; beforeDeserialize ? : IOProto ; afterDeserialize ? : IOProto ; } type BaseMetadata = { required ? : boolean ; } & BeforeAfterProto ; type Args = string | ( { name ? : string ; type ? : Function ; isDictionary ? : boolean ; } & BaseMetadata ) | ( { name ? : string ; predicate ? : PredicateProto ; isDictionary ? : boolean ; } & BaseMetadata ) | ( { names : Array < string > ; type ? : Function ; } & BaseMetadata ) | ( { names : Array < string > ; predicate ? : PredicateProto ; } & BaseMetadata ) ; type Metadata = ( { name : string ; type ? : Function ; isDictionary ? : boolean ; isNameOverridden : boolean ; } & BaseMetadata ) | ( { name : string ; predicate : PredicateProto ; isDictionary ? : boolean ; isNameOverridden : boolean ; } & BaseMetadata ) | ( { names : Array < string > ; type : Function ; } & BaseMetadata ) | ( { names : Array < string > ; predicate : PredicateProto ; } & BaseMetadata ) ; interface MetadataMap { [ id ] : Metadata ; } param { Object } target returns { Array < string > } const getBaseClassNames = ( target ) : Array < string > => { const baseClass = Reflect . getPrototypeOf ( target ) ; if ( ! baseClass || ! baseClass [ 'name' ] ) { return [ ] ; } return [ ... getBaseClassNames ( baseClass ) , baseClass [ 'name' ] ] ; } ; param { object } ctor returns { Array < string > } const getPropertyNames = ( ctor ) : Map < number , string > => { const ctorWithoutClassBody = ctor . toString ( ) . split ( '}' ) [ 0 ] ; const ctorWithoutComments = ctorWithoutClassBody . replace ( /(\/\*[\s\S]*?\*\/|\/\/.*$)/gm , '' ) ; const ctorOnSingleLine = ctorWithoutComments . replace ( /[\r\t\n\v\f ]/g , '' ) ; const constructorParamPattern = /(?:.*(?:constructor|function).*?(?=\())(?:\()(.+?(?=\)))/m ; const propertyPattern = /(?:this\.)([^,;\n}]+)/gm ; const propertyNames = new Map < number , string > ( ) ; const paramsExecArray = constructorParamPattern . exec ( ctorOnSingleLine ) ; if ( ! paramsExecArray || ! paramsExecArray . length ) { return propertyNames ; } const params = paramsExecArray [ 1 ] . split ( ',' ) ; let match ; while ( ( match = propertyPattern . exec ( ctorOnSingleLine ) ) ) { const matchResult = match [ 1 ] . split ( '=' ) ; const index = params . findIndex ( param => param === matchResult [ 1 ] ) ; if ( index > - 1 ) { propertyNames . set ( index , matchResult [ 0 ] ) ; } } return propertyNames ; } ; param { Args = } args export const JsonProperty = ( args ? ) : Function => ( target , key , index ) : void => { if ( key === undefined && target [ 'prototype' ] ) { const type = Reflect . getMetadata ( designParamTypes , target ) [ index ] ; const keys = getPropertyNames ( target [ 'prototype' ] . constructor ) ; key = keys . get ( index ) as string ; target = target [ 'prototype' ] ; Reflect . defineMetadata ( designType , type , target , key ) ; } let map = { } ; const targetName = target . constructor . name ; const apiMapTargetName = `${ apiMap }${ targetName }` ; if ( Reflect . hasMetadata ( apiMapTargetName , target ) ) { map = Reflect . getMetadata ( apiMapTargetName , target ) ; } map [ key ] = getMetadata ( key , args as Args ) ; Reflect . defineMetadata ( apiMapTargetName , map , target ) ; } ; param { { formatPropertyNames : FormatPropertyNameProto } = } options export const Serializable = ( options ? ) : Function => ( target ) => { const baseClassNames = getBaseClassNames ( target ) ; Reflect . defineMetadata ( apiMapSerializable , { baseClassNames , options } , target ) ; } ; param baseClassNames param { any } instance returns { MetadataMap } const getBaseClassMetadataMaps = ( baseClassNames , instance ) : Array < MetadataMap > => baseClassNames . map ( baseClassName => Reflect . getMetadata ( `${ apiMap }${ baseClassName }` , instance ) ) ; param metadataMaps returns const mergeMetadataMap = ( ... metadataMaps ) : MetadataMap => { const mergedMetadataMap = { } ; metadataMaps . forEach ( metadataMap => { if ( metadataMap ) { Object . keys ( metadataMap ) . forEach ( key => { mergedMetadataMap [ key ] = { ... mergedMetadataMap [ key ] , ... metadataMap [ key ] } ; } ) ; } } ) ; return mergedMetadataMap ; } ; param { object } json param { new ( ... params : Array < any > ) => T } type returns { T } export const deserialize = < T > ( json , type ) : T => { if ( [ null , undefined ] . includes ( json ) ) { return json as never ; } if ( type === undefined ) { return castSimpleData ( typeof json , json ) ; } if ( typeof json === Type . String ) { json = JSON . parse ( json ) ; } const instance = new type ( { } ) ; const instanceName = instance . constructor . name ; const { baseClassNames , options } = ( Reflect . getMetadata ( apiMapSerializable , type ) as SerializableMetadata ) ?? { } ; const apiMapInstanceName = `${ apiMap }${ instanceName }` ; const hasMap = Reflect . hasMetadata ( apiMapInstanceName , instance ) ; let instanceMap = { } ; if ( ! hasMap && ( ! baseClassNames || ! baseClassNames . length ) ) { return instance ; } instanceMap = Reflect . getMetadata ( apiMapInstanceName , instance ) ; if ( baseClassNames && baseClassNames . length ) { const baseClassMetadataMaps = getBaseClassMetadataMaps ( baseClassNames , instance ) ; instanceMap = mergeMetadataMap ( ... baseClassMetadataMaps , instanceMap ) ; } Object . keys ( instanceMap ) . forEach ( key => { const property = convertDataToProperty ( instance , key , instanceMap [ key ] , json , options ?. formatPropertyNames ) ; if ( ( property === undefined || property === null ) && instanceMap [ key ] . required ) { throw new Error ( `Property '${ key }' is required in ${ instanceName } ${ JSON . stringify ( json ) }.` ) ; } if ( property !== undefined ) { instance [ key ] = property ; } } ) ; return instance ; } ; param { any } instance param { boolean } removeUndefined returns { any } export const serialize = ( instance , removeUndefined = true ) : any => { if ( [ undefined , null ] . includes ( instance ) || typeof instance !== Type . Object ) { return instance ; } const instanceName = instance . constructor . name ; const apiMapInstanceName = `${ apiMap }${ instanceName }` ; const { baseClassNames , options } = ( Reflect . getMetadata ( apiMapSerializable , instance . constructor ) as SerializableMetadata ) ?? { } ; const hasBaseClasses = baseClassNames && baseClassNames . length ; const hasMap = Reflect . hasMetadata ( apiMapInstanceName , instance ) ; let instanceMap = { } ; if ( ! hasMap && ! hasBaseClasses ) { return instance ; } instanceMap = Reflect . getMetadata ( apiMapInstanceName , instance ) ; if ( hasBaseClasses ) { const baseClassMetadataMaps = getBaseClassMetadataMaps ( baseClassNames , instance ) ; instanceMap = mergeMetadataMap ( ... baseClassMetadataMaps , instanceMap ) ; } const json = { } ; const instanceKeys = Object . keys ( instance ) ; Object . keys ( instanceMap ) . forEach ( key => { if ( instanceKeys . includes ( key ) ) { const metadata = instanceMap [ key ] ; const beforeSerialize = metadata [ 'beforeSerialize' ] ; const afterSerialize = metadata [ 'afterSerialize' ] ; let initialValue ; if ( beforeSerialize ) { initialValue = instance [ key ] ; instance [ key ] = beforeSerialize ( instance [ key ] , instance ) ; } let data = convertPropertyToData ( instance , key , metadata , removeUndefined ) ; if ( afterSerialize ) { data = afterSerialize ( data , instance ) ; } instance [ key ] = initialValue || instance [ key ] ; if ( metadata [ 'names' ] ) { metadata [ 'names' ] . forEach ( ( name ) => { if ( ! removeUndefined || ( removeUndefined && data [ name ] !== undefined ) ) { json [ name ] = data [ name ] ; } } ) ; } else { if ( ! removeUndefined || ( removeUndefined && data !== undefined ) ) { if ( ! metadata [ 'isNameOverridden' ] && options ?. formatPropertyNames ) { const name = options . formatPropertyNames ( metadata [ 'name' ] ) ; json [ name ] = data ; } else { json [ metadata [ 'name' ] ] = data ; } } } } } ) ; return json ; } ; param { Function } instance param { string } key param { Metadata } metadata param { boolean } removeUndefined returns { any } const convertPropertyToData = ( instance , key , metadata , removeUndefined ) : any => { const property = instance [ key ] ; const type = Reflect . getMetadata ( designType , instance , key ) ; const isArray = type . name ? type . name . toLocaleLowerCase ( ) === Type . Array : false ; const predicate = metadata [ 'predicate' ] ; const propertyType = metadata [ 'type' ] || type ; const isSerializableProperty = isSerializable ( propertyType ) ; if ( property && ( isSerializableProperty || predicate ) ) { if ( isArray ) { return property . map ( ( d ) => serialize ( d , removeUndefined ) ) ; } if ( metadata [ 'isDictionary' ] ) { const obj = { } ; Object . keys ( property ) . forEach ( k => { obj [ k ] = serialize ( property [ k ] , removeUndefined ) ; } ) ; return obj ; } return serialize ( property , removeUndefined ) ; } if ( propertyType . name . toLocaleLowerCase ( ) === Type . Date && typeof property === Type . Object ) { return property . toISOString ( ) ; } return property ; } ; param { Function } instance param { string } key param { Metadata } metadata param { any } json const convertDataToProperty = ( instance , key , metadata , json , formatPropertyName ? ) => { let data ; if ( [ null , undefined ] . includes ( json ) ) { return json ; } if ( 'names' in metadata ) { const object = { } ; metadata . names . forEach ( ( name ) => ( object [ name ] = json [ name ] ) ) ; data = object ; } else if ( 'name' in metadata && ! metadata . isNameOverridden && formatPropertyName ) { const name = formatPropertyName ( metadata . name ) ; data = json [ name ] ; } else { data = json [ metadata . name ] ; } if ( [ null , undefined ] . includes ( data ) ) { return data ; } const type = Reflect . getMetadata ( designType , instance , key ) ; const isArray = type . name ? type . name . toLowerCase ( ) === Type . Array : false ; const isDictionary = metadata [ 'isDictionary' ] ; const predicate = metadata [ 'predicate' ] ; const beforeDeserialize = metadata [ 'beforeDeserialize' ] ; const afterDeserialize = metadata [ 'afterDeserialize' ] ; let propertyType = metadata [ 'type' ] || type ; const isSerializableProperty = isSerializable ( propertyType ) ; let result ; if ( beforeDeserialize ) { data = beforeDeserialize ( data , instance ) ; } if ( isDictionary ) { const obj = { } ; if ( typeof data !== Type . Object ) { console . error ( `Type '${ typeof data }' is not assignable to type 'Dictionary' for property '${ key }' in '${ instance . constructor . name }'.\n` , `Received: ${ JSON . stringify ( data ) }` ) ; result = undefined ; } else { Object . keys ( data ) . forEach ( k => { if ( ! isSerializableProperty && ! predicate ) { obj [ k ] = castSimpleData ( typeof data [ k ] , data [ k ] , key , instance . constructor . name ) ; } else { if ( predicate ) { propertyType = predicate ( data [ k ] , data ) ; } obj [ k ] = deserialize ( data [ k ] , propertyType ) ; } } ) ; result = obj ; } } else if ( isArray ) { const array = [ ] ; if ( ! Array . isArray ( data ) ) { console . error ( `Type '${ typeof data }' is not assignable to type 'Array' for property '${ key }' in '${ instance . constructor . name }'.\n` , `Received: ${ JSON . stringify ( data ) }` ) ; result = undefined ; } else { data . forEach ( ( d ) => { if ( ! isSerializableProperty && ! predicate ) { array . push ( castSimpleData ( typeof d , d , key , instance . constructor . name ) ) ; } else { if ( predicate ) { propertyType = predicate ( d , data ) ; } array . push ( deserialize ( d , propertyType ) ) ; } } ) ; result = array ; } } else if ( ! isSerializableProperty && ! predicate ) { result = castSimpleData ( propertyType . name , data , key , instance . constructor . name ) ; } else { propertyType = predicate ? predicate ( data , json ) : propertyType ; result = deserialize ( data , propertyType ) ; } if ( afterDeserialize ) { result = afterDeserialize ( result , instance ) ; } return result ; } ; param { any } type returns { boolean } const isSerializable = ( type ) : boolean => Reflect . hasOwnMetadata ( apiMapSerializable , type ) ; param { string } key param { Args } args returns { Metadata } const getMetadata = ( key , args ) : Metadata => { if ( ! args ) { return { name : key . toString ( ) , isNameOverridden : false } as Metadata ; } let metadata ; if ( typeof args === Type . String ) { metadata = { name : args , isNameOverridden : true } ; } else if ( args [ 'name' ] ) { metadata = { name : args [ 'name' ] , isNameOverridden : true } ; } else if ( args [ 'names' ] && args [ 'names' ] . length ) { metadata = { names : args [ 'names' ] } ; } else { metadata = { name : key . toString ( ) , isNameOverridden : false } ; } const optionalArgKeys = [ 'isDictionary' , 'required' , 'beforeSerialize' , 'afterSerialize' , 'beforeDeserialize' , 'afterDeserialize' ] ; optionalArgKeys . forEach ( k => { if ( args [ k ] !== undefined && args [ k ] !== null ) { metadata [ k ] = args [ k ] ; } } ) ; if ( args [ 'predicate' ] ) { metadata . predicate = args [ 'predicate' ] ; } else if ( args [ 'type' ] ) { metadata . type = args [ 'type' ] ; } return metadata as Metadata ; } ; param { string } type param { any } data returns { any } const castSimpleData = ( type , data , propertyName ? , className ? ) : any => { if ( type === undefined || type === null ) { return data ; } type = type . toLowerCase ( ) ; if ( ( typeof data ) . toLowerCase ( ) === type ) { return data ; } const logError = ( ) => { console . error ( `Type '${ typeof data }' is not assignable to type '${ type }' for property '${ propertyName }' in '${ className }'.\n` , `Received: ${ JSON . stringify ( data ) }` ) ; } ; switch ( type ) { case Type . String : const string = data . toString ( ) ; if ( string === '[object Object]' ) { logError ( ) ; return undefined ; } return string ; case Type . Number : const number = + data ; if ( isNaN ( number ) ) { logError ( ) ; return undefined ; } return number ; case Type . Boolean : logError ( ) ; return undefined ; case Type . Date : if ( isNaN ( Date . parse ( data ) ) ) { logError ( ) ; return undefined ; } return new Date ( data ) ; default : return data ; } } ;