<!DOCTYPE html>
<html>
  <head>
    <title>Конвертер на декораторах TypeScript</title>
  </head>
  <body>
    <h3>Input JSON:</h3>
    <pre id="jsonInput"></pre>
    <h3>Parse result:</h3>
    <pre id="parseResult"></pre>
    <h3>Serialize result:</h3>
    <pre id="serializeResult"></pre>
  </body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/reflect-metadata/0.1.8/Reflect.min.js"></script>
  <script src="Script.js"></script>
  <script>
var jsonData = 
`{ 
    "username": "PFight77", 
    "email": "test@gmail.com", 
    "doc": { 
      "info": "Author of the article"
    }
}`;
    document.getElementById("jsonInput").innerText = jsonData;
    
    // Test parser
    var user = UserInfo.parse(jsonData);
    document.getElementById("parseResult").innerText = JSON.stringify(user, null, 4);
    var serializedJSON = user.serialize();
    document.getElementById("serializeResult").innerText = serializedJSON;
  </script>
</html>
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true
  }
}
// Объявляем уникальные ключи, по которым будем идентифицировать наши метаданные
const ServerNameMetadataKey = "Habrahabr_PFight77_ServerName";
const AvailableFieldsMetadataKey = "Habrahabr_PFight77_AvailableFields";
// Объявляем декоратор
function ServerModelField(name?: string) {
    return (target: Object, propertyKey: string) => {
        // Сохраняем в метаданных переднный name, либо название самого свойства, если параметр не задан
        Reflect.defineMetadata(ServerNameMetadataKey, name || propertyKey, target, propertyKey);
        // Проверяем, не определены ли уже availableFields другим экземпляром декоратора
        var availableFields = Reflect.getMetadata(AvailableFieldsMetadataKey, target);
        if (!availableFields) {
            // Ok, мы первые, значит создаем новый массив
            availableFields = [];
            // Не передаем 4-й параметр(propertyKey) в defineMetadata, 
            // т.к. метаданные общие для всех полей
            Reflect.defineMetadata(AvailableFieldsMetadataKey, availableFields, target);            
        }
        // Регистрируем текущее поле в метаданных
        availableFields.push(propertyKey);
    }
}

function convertFromServer<T>(serverObj: Object, type: { new(): T ;} ): T {
    // Создаем объект, с помощью конструктора, переданного в параметре type
    var clientObj: T = new type();
    // Получаем контейнер с метаданными
    var target = Object.getPrototypeOf(clientObj);
    // Получаем из метаданных, какие декорированные свойства есть в классе
    var availableNames = Reflect.getMetadata(AvailableFieldsMetadataKey, target) as [string];
    if (availableNames) {
        // Обрабатываем каждое свойство
        availableNames.forEach(propName => {
            // Получаем из метаданных имя свойства в JSON
            var serverName = Reflect.getMetadata(ServerNameMetadataKey, target, propName);
            if (serverName) {
                // Получаем значение, переданное сервером
                var serverVal = serverObj[serverName];
                if (serverVal) {
                    var clientVal = null;
                    // Проверяем, используются ли в классе свойства декораторы @ServerModelField
                    // Получаем конструктор класса
                    var propType = Reflect.getMetadata("design:type", target, propName);
                    // Смотрим, есть ли в метаданных класса информация о свойствах
                    var propTypeServerFields =  Reflect.getMetadata(AvailableFieldsMetadataKey, propType.prototype) as [string];
                    if (propTypeServerFields) {
                        // Да, класс использует наш декоратор, обрабатываем свойство рекурсивно
                        clientVal = convertFromServer(serverVal, propType);
                    } else {
                        // Нет, просто копируем значение
                        clientVal = serverVal;
                    }
                    // Записываем результат в конечный объект
                    clientObj[propName] = clientVal;
                }
            }
        });
    } else {
        errorNoPropertiesFound(getTypeName(type));
    }

    return clientObj;
}

function convertToServer<T>(clientObj: T): Object {
    var serverObj = {};

    var target = Object.getPrototypeOf(clientObj);
    var availableNames = Reflect.getMetadata(AvailableFieldsMetadataKey, target) as [string];
    availableNames.forEach(propName=> {        
        var serverName = Reflect.getMetadata(ServerNameMetadataKey, target, propName);
        if (serverName) {
            var clientVal = clientObj[propName];
            if (clientVal) {
                var serverVal = null;
                var propType = Reflect.getMetadata("design:type", target, propName);
                var propTypeServerFields =  Reflect.getMetadata(AvailableFieldsMetadataKey, propType.prototype) as [string];
                if (clientVal && propTypeServerFields) {
                    serverVal = convertToServer(clientVal);
                } else {
                    serverVal = clientVal;
                }
                serverObj[serverName] = serverVal;
            }
        }
    });

    if (!availableNames) {
        errorNoPropertiesFound(parseTypeName(clientObj.constructor.toString()));
    }

    return serverObj;
}


class UserAdditionalInfo {
    @ServerModelField("info")
    public mRole: string;
}
class UserInfo {
    @ServerModelField("username")
    private mUserName: string;
    @ServerModelField("email")
    private mEmail: string;
    @ServerModelField("doc")
    private mAdditionalInfo: UserAdditionalInfo;
    
    public get DisplayName() {
        return mUserName + " " + mAdditionalInfo.mRole;
    }
    public get ID() {
        return mEmail;
    }    
    public static parse(jsonData: string): UserInfo {
        return convertFromServer(JSON.parse(jsonData), UserInfo);
    }
    
    public serialize(): string {
        var serverData = convertToServer(this);
        return JSON.stringify(serverData, null, 4);
    }
}