Как преобразовать объект JSON в класс машинописи

Я прочитал объект JSON с удаленного сервера REST. Этот JSON-объект обладает всеми свойствами класса машинописного текста (по замыслу). Как преобразовать полученный полученный объект JSON в тип var?

Я не хочу заполнять машинописный текст var (то есть иметь конструктор, который принимает этот объект JSON). Он большой, и копирование всего через подобъект по подобъекту и свойству по свойству займет много времени.

Обновление: однако вы можете привести его к интерфейсу машинописи!

33 ответа

Решение

Вы не можете просто преобразовать результат простого старого JavaScript из запроса Ajax в прототипный экземпляр класса JavaScript/TypeScript. Для этого существует ряд методов, которые обычно включают копирование данных. Если вы не создадите экземпляр класса, у него не будет никаких методов или свойств. Это останется простым объектом JavaScript.

Хотя, если бы вы имели дело только с данными, вы могли бы просто выполнить приведение к интерфейсу (поскольку это просто структура времени компиляции), для этого потребовалось бы использовать класс TypeScript, который использует экземпляр данных и выполняет операции с этими данными.

Некоторые примеры копирования данных:

  1. Копирование объекта AJAX JSON в существующий объект
  2. Разобрать строку JSON в прототип конкретного объекта в JavaScript

По сути, вы бы просто:

var d = new MyRichObject();
d.copyInto(jsonResult);

У меня была та же проблема, и я нашел библиотеку, которая выполняет эту работу: https://github.com/pleerock/class-transformer.

Это работает так:

        let jsonObject = response.json() as Object;
        let fooInstance = plainToClass(Models.Foo, jsonObject);
        return fooInstance;

Он поддерживает вложенные дочерние элементы, но вы должны украсить члена вашего класса.

В TypeScript вы можете сделать утверждение типа с помощью интерфейса и обобщений, например, так:

var json = Utilities.JSONLoader.loadFromFile("../docs/location_map.json");
var locations: Array<ILocationMap> = JSON.parse(json).location;

Где ILocationMap описывает форму ваших данных. Преимущество этого метода в том, что ваш JSON может содержать больше свойств, но форма удовлетворяет условиям интерфейса.

Надеюсь, это поможет!

Если вы используете ES6, попробуйте это:

class Client{
  name: string

  displayName(){
    console.log(this.name)
  }
}

service.getClientFromAPI().then(clientData => {

  // Here the client data from API only have the "name" field
  // If we want to use the Client class methods on this data object we need to:
  let clientWithType = Object.assign(new Client(), clientData)

  clientWithType.displayName()
})

Но этот путь не будет работать на объекте гнезда, к сожалению.

Я нашел очень интересную статью о приведении JSON к классу Typescript:

http://cloudmark.github.io/Json-Mapping/

Вы получите следующий код:

let example = {
                "name": "Mark", 
                "surname": "Galea", 
                "age": 30, 
                "address": {
                  "first-line": "Some where", 
                  "second-line": "Over Here",
                  "city": "In This City"
                }
              };

MapUtils.deserialize(Person, example);  // custom class

Автоматически проверять, есть ли у объекта JSON, полученного с сервера, ожидаемые (считаются в соответствии с) свойства интерфейса машинописного текста, пока нечего. Но вы можете использовать Определяемые пользователем Стражи Типа

Учитывая следующий интерфейс и глупый объект json (это мог быть любой тип):

interface MyInterface {
    key: string;
 }

const json: object = { "key": "value" }

Три возможных способа:

A. Утверждение типа или простое статическое приведение после переменной

const myObject: MyInterface = json as MyInterface;

B. Простое статическое приведение, перед переменной и между алмазами

const myObject: MyInterface = <MyInterface>json;

C. Продвинутый динамический состав, вы сами проверяете структуру объекта

function isMyInterface(json: any): json is MyInterface {
    // silly condition to consider json as conform for MyInterface
    return typeof json.key === "string";
}

if (isMyInterface(json)) {
    console.log(json.key)
}
else {
        throw new Error(`Expected MyInterface, got '${json}'.`);
}

Вы можете поиграть с этим примером здесь

Обратите внимание, что трудность здесь заключается в том, чтобы написать isMyInterface функция. Я надеюсь, что TS добавит декоратор рано или поздно, чтобы экспортировать сложную типизацию во время выполнения и позволить среде выполнения проверять структуру объекта при необходимости. На данный момент вы можете использовать валидатор схемы json, цель которого примерно такая же, ИЛИ этот генератор функций проверки типа во время выполнения.

TLDR: один лайнер

// This assumes your constructor method will assign properties from the arg.
.map((instanceData: MyClass) => new MyClass(instanceData));

Подробный ответ

Я бы не рекомендовал подход Object.assign, так как он может ненадлежащим образом засорять ваш экземпляр класса нерелевантными свойствами (а также определенными замыканиями), которые не были объявлены внутри самого класса.

В классе, в который вы пытаетесь десериализоваться, я бы гарантировал, что все свойства, которые вы хотите десериализовать, определены (нуль, пустой массив и т. Д.). Определяя ваши свойства с начальными значениями, вы открываете их видимость при попытке перебрать членов класса для назначения значений (см. Метод десериализации ниже).

export class Person {
  public name: string = null;
  public favoriteSites: string[] = [];

  private age: number = null;
  private id: number = null;
  private active: boolean;

  constructor(instanceData?: Person) {
    if (instanceData) {
      this.deserialize(instanceData);
    }
  }

  private deserialize(instanceData: Person) {
    // Note this.active will not be listed in keys since it's declared, but not defined
    const keys = Object.keys(this);

    for (const key of keys) {
      if (instanceData.hasOwnProperty(key)) {
        this[key] = instanceData[key];
      }
    }
  }
}

В приведенном выше примере я просто создал метод десериализации. В реальном примере я бы централизовал его в повторно используемом базовом классе или методе обслуживания.

Вот как использовать это в чем-то вроде http соответственно...

this.http.get(ENDPOINT_URL)
  .map(res => res.json())
  .map((resp: Person) => new Person(resp) ) );

Если tslint / ide жалуется на несовместимость типа аргумента, просто приведите аргумент к тому же типу, используя угловые скобки <YourClassName>, пример:

const person = new Person(<Person> { name: 'John', age: 35, id: 1 });

Если у вас есть члены класса определенного типа (иначе: экземпляр другого класса), вы можете привести их к типизированным экземплярам с помощью методов getter / setter.

export class Person {
  private _acct: UserAcct = null;
  private _tasks: Task[] = [];

  // ctor & deserialize methods...

  public get acct(): UserAcct {
    return this.acct;
  }
  public set acct(acctData: UserAcct) {
    this._acct = new UserAcct(acctData);
  }

  public get tasks(): Task[] {
    return this._tasks;
  }

  public set tasks(taskData: Task[]) {
    this._tasks = taskData.map(task => new Task(task));
  }
}

Приведенный выше пример десериализует как acct, так и список задач в соответствующие экземпляры классов.

Предполагая, что json имеет те же свойства, что и ваш класс машинописи, вам не нужно копировать свойства Json в объект машинописи. Вам просто нужно создать свой объект Typescript, передавая данные json в конструктор.

В вашем обратном вызове ajax вы получаете компанию:

onReceiveCompany( jsonCompany : any ) 
{
   let newCompany = new Company( jsonCompany );

   // call the methods on your newCompany object ...
}

Для того, чтобы сделать эту работу:

1) Добавьте конструктор в свой класс Typescript, который принимает данные json в качестве параметра. В этом конструкторе вы расширяете свой объект json с помощью jQuery, например так: $.extend( this, jsonData), $.extend позволяет сохранить прототипы javascript при добавлении свойств объекта json.

2) Обратите внимание, что вы должны будете сделать то же самое для связанных объектов. В случае Employees в этом примере вы также создаете конструктор, который принимает часть данных json для сотрудников. Вы вызываете $.map для перевода сотрудников json в машинописные объекты Employee.

export class Company
{
    Employees : Employee[];

    constructor( jsonData: any )
    {
        $.extend( this, jsonData);

        if ( jsonData.Employees )
            this.Employees = $.map( jsonData.Employees , (emp) => {
                return new Employee ( emp );  });
    }
}

export class Employee
{
    name: string;
    salary: number;

    constructor( jsonData: any )
    {
        $.extend( this, jsonData);
    }
}

Это лучшее решение, которое я нашел при работе с классами Typescript и объектами json.

В моем случае это работает. Я использовал функции Object.assign (цель, источники...). Сначала создается правильный объект, затем копируются данные из объекта json в цель. Пример:

let u:User = new User();
Object.assign(u , jsonUsers);

И более продвинутый пример использования. Пример использования массива.

this.someService.getUsers().then((users: User[]) => {
  this.users = [];
  for (let i in users) {
    let u:User = new User();
    Object.assign(u , users[i]);
    this.users[i] = u;
    console.log("user:" + this.users[i].id);
    console.log("user id from function(test it work) :" + this.users[i].getId());
  }

});

export class User {
  id:number;
  name:string;
  fullname:string;
  email:string;

  public getId(){
    return this.id;
  }
}

Пока это не кастинг за слово; Я нашел https://github.com/JohnWhiteTB/TypedJSON полезной альтернативой.

@JsonObject
class Person {
    @JsonMember
    firstName: string;

    @JsonMember
    lastName: string;

    public getFullname() {
        return this.firstName + " " + this.lastName;
    }
}
var person = TypedJSON.parse('{ "firstName": "John", "lastName": "Doe" }', Person);

person instanceof Person; // true
person.getFullname(); // "John Doe"

Используйте объявление as:

const data = JSON.parse(response.data) as MyClass;

Лично я считаю ужасным, что машинописный текст не позволяет определению конечной точки указывать тип получаемого объекта. Похоже, что это действительно так, я бы сделал то, что делал с другими языками, а именно, что я бы отделил объект JSON от определения класса, и чтобы определение класса использовало объект JSON в качестве единственного члена данных..

Я презираю шаблонный код, поэтому для меня это обычно вопрос достижения желаемого результата с наименьшим количеством кода при сохранении типа.

Рассмотрим следующие определения структуры объекта JSON - это то, что вы получили бы в конечной точке, это только определения структуры, без методов.

interface IAddress {
    street: string;
    city: string;
    state: string;
    zip: string;
}

interface IPerson {
    name: string;
    address: IAddress;
}

Если мы подумаем о вышесказанном в объектно-ориентированных терминах, указанные выше интерфейсы не являются классами, потому что они определяют только структуру данных. Класс в терминах объектно-ориентированного программирования определяет данные и код, который с ними работает.

Итак, теперь мы определяем класс, который определяет данные и код, который с ними работает...

class Person {
    person: IPerson;

    constructor(person: IPerson) {
        this.person = person;
    }

    // accessors
    getName(): string {
        return person.name;
    }

    getAddress(): IAddress {
        return person.address;
    }

    // You could write a generic getter for any value in person, 
    // no matter how deep, by accepting a variable number of string params

    // methods
    distanceFrom(address: IAddress): float {
        // Calculate distance from the passed address to this persons IAddress
        return 0.0;
    }
}

А теперь мы можем просто передать любой объект, соответствующий структуре IPerson, и продолжить свой путь...

   Person person = new Person({
            name: "persons name",
            address: {
                street: "A street address",
                city: "a city",
                state: "a state",
                zip: "A zipcode"
            }
        });

Таким же образом мы теперь можем обработать объект, полученный на вашей конечной точке, с чем-то вроде...

Person person = new Person(req.body);    // As in an object received via a POST call

person.distanceFrom({ street: "Some street address", etc.});

Это гораздо более производительно и использует половину памяти для копирования данных, при этом значительно сокращая объем стандартного кода, который вы должны написать для каждого типа сущности. Он просто полагается на безопасность типов, обеспечиваемую TypeScript.

Если вам нужно преобразовать свой json-объект в класс машинописного текста и иметь его методы экземпляра, доступные в результирующем объекте, вам необходимо использовать Object.setPrototypeOf, как я сделал в приведенном ниже фрагменте кода:

Object.setPrototypeOf(jsonObject, YourTypescriptClass.prototype)

Используйте класс, расширенный из интерфейса.

Затем:

Object.assign(
                new ToWhat(),
                what
              )

И лучше всего:

Object.assign(
                    new ToWhat(),
                    <IDataInterface>what
                  )

ToWhat становится контроллером DataInterface.

Вместо класса вы можете использовать интерфейс TypeScript.

Допустим, мы работаем с REST API, который возвращает строку JSON, представляющую пользователя. JSON API соглашается с контрактом, что он возвращает строку firstName, строку lastName и числовой идентификатор. Мы моделируем пользователя как интерфейс в TypeScript следующим образом:

      interface User {
   id: number;
   firstName: string;
   lastName: string;
}

Следующий код подойдет, если у вас есть правильная карта атрибутов:

      const json = '{"id": 1, "firstName": "Bob", "lastName": "Dylan"}'; //this will come from the REST API
try {
  const user: User = JSON.parse(json);
  console.log(user);
} catch (e) {
  console.log("An error on casting object has occurred", e);
}

Результат:

      [LOG]: {
  "id": 1,
  "firstName": "Bob",
  "lastName": "Dylan"
} 

Воспроизвести этот код здесь

Старый вопрос с в основном правильными, но не очень эффективными ответами. Вот что я предлагаю:

Создайте базовый класс, который содержит метод init () и статические методы приведения (для одного объекта и массива). Статические методы могут быть где угодно; версия с базовым классом и init () впоследствии позволяет легко расширяться.

export class ContentItem {
    // parameters: doc - plain JS object, proto - class we want to cast to (subclass of ContentItem)
    static castAs<T extends ContentItem>(doc: T, proto: typeof ContentItem): T {
        // if we already have the correct class skip the cast
        if (doc instanceof proto) { return doc; }
        // create a new object (create), and copy over all properties (assign)
        const d: T = Object.create(proto.prototype);
        Object.assign(d, doc);
        // reason to extend the base class - we want to be able to call init() after cast
        d.init(); 
        return d;
    }
    // another method casts an array
    static castAllAs<T extends ContentItem>(docs: T[], proto: typeof ContentItem): T[] {
        return docs.map(d => ContentItem.castAs(d, proto));
    }
    init() { }
}

Подобная механика (с assign ()) была упомянута в посте @Adam111p. Просто еще один (более полный) способ сделать это. @Timothy Perez критически относится к assign (), но imho он здесь вполне уместен.

Реализуйте производный (настоящий) класс:

import { ContentItem } from './content-item';

export class SubjectArea extends ContentItem {
    id: number;
    title: string;
    areas: SubjectArea[]; // contains embedded objects
    depth: number;

    // method will be unavailable unless we use cast
    lead(): string {
        return '. '.repeat(this.depth);
    }

    // in case we have embedded objects, call cast on them here
    init() {
        if (this.areas) {
            this.areas = ContentItem.castAllAs(this.areas, SubjectArea);
        }
    }
}

Теперь мы можем привести объект, полученный из сервиса:

const area = ContentItem.castAs<SubjectArea>(docFromREST, SubjectArea);

Вся иерархия объектов SubjectArea будет иметь правильный класс.

Вариант использования / пример; создайте сервис Angular (снова абстрактный базовый класс):

export abstract class BaseService<T extends ContentItem> {
  BASE_URL = 'http://host:port/';
  protected abstract http: Http;
  abstract path: string;
  abstract subClass: typeof ContentItem;

  cast(source: T): T {
    return ContentItem.castAs(source, this.subClass);
  }
  castAll(source: T[]): T[] {
    return ContentItem.castAllAs(source, this.subClass);
  }

  constructor() { }

  get(): Promise<T[]> {
    const value = this.http.get(`${this.BASE_URL}${this.path}`)
      .toPromise()
      .then(response => {
        const items: T[] = this.castAll(response.json());
        return items;
      });
    return value;
  }
}

Использование становится очень простым; создать сервис Area:

@Injectable()
export class SubjectAreaService extends BaseService<SubjectArea> {
  path = 'area';
  subClass = SubjectArea;

  constructor(protected http: Http) { super(); }
}

Методget () сервиса вернет Promise для массива, уже приведенного как объекты SubjectArea (вся иерархия)

Теперь скажите, у нас есть другой класс:

export class OtherItem extends ContentItem {...}

Создать сервис, который извлекает данные и приводит к правильному классу, так же просто, как:

@Injectable()
export class OtherItemService extends BaseService<OtherItem> {
  path = 'other';
  subClass = OtherItem;

  constructor(protected http: Http) { super(); }
}

Вы можете создать interface вашего типа (SomeType) и бросить объект в этом.

const typedObject: SomeType = <SomeType> responseObject;

ДЛЯ ЛЮБИТЕЛЕЙ JAVA

Сделать класс POJO

      export default class TransactionDTO{
    constructor() {
    }
}

создать пустой объект по классу

      let dto = new TransactionDto()   // ts object
let json = {name:"Kamal",age:40} // js object

let transaction: TransactionDto = Object.assign(dto,JSON.parse(JSON.stringify(json)));//conversion

https://jvilk.com/MakeTypes/

вы можете использовать этот сайт для создания прокси-сервера для вас. он генерирует класс и может анализировать и проверять ваш входной объект JSON.

Вы можете передать json в свойство, подобное этому

class Jobs {
  constructor(JSONdata) {
    this.HEAT = JSONdata.HEAT;    
    this.HEAT_EAF = JSONdata.HEAT_EAF;    
  }
  
}

 var job = new Jobs({HEAT:'123',HEAT_EAF:'456'});

Я использовал эту библиотеку здесь: https://github.com/pleerock/class-transformer

<script lang="ts">
    import { plainToClass } from 'class-transformer';
</script>

Реализация:

private async getClassTypeValue() {
  const value = await plainToClass(ProductNewsItem, JSON.parse(response.data));
}

Иногда вам нужно будет проанализировать значения JSON для plainToClass, чтобы понять, что это данные в формате JSON

Я создал простой инструмент для этого https://beshanoe.github.io/json2ts/ В отличие от json2ts.com, он работает непосредственно в браузере и не отправляет ваши данные на неизвестные серверы. Также он имеет несколько настроек. Я буду работать, чтобы улучшить его функциональность

В конце TS вы можете сделать так:

const isMyInterface = (val: any): val is MyInterface => {
  if (!val) { return false; }
  if (!val.myProp) { return false; }
  return true;
};

И чем пользователь, как это:

if (isMyInterface(data)) {
 // now data will be type of MyInterface
}

Вместо класса вы можете использовать интерфейс TypeScript.

Допустим, мы работаем с REST API, который возвращает строку JSON, представляющую пользователя. JSON API соглашается с контрактом, что он возвращает строку firstName, строку lastName и числовой идентификатор. Мы моделируем пользователя как интерфейс в TypeScript следующим образом:

      interface User {
   id: number;
   firstName: string;
   lastName: string;
}

Следующий код подойдет, если у вас есть правильная карта атрибутов:

      const json = '{"id": 1, "firstName": "Bob", "lastName": "Dylan"}'; //this will come from the REST API
try {
  const user: User = JSON.parse(json);
  console.log(user);
} catch (e) {
  console.log("An error on casting object has occurred", e);
}

Результат:

      [LOG]: {
  "id": 1,
  "firstName": "Bob",
  "lastName": "Dylan"
} 

Воспроизвести этот код здесь

Я не вижу упоминания о json-typcript-mapper: https://www.npmjs.com/package/json-typescript-mapper. Насколько я могу судить, это похоже на сочетание находки @PhilipMiglinci и ответа @Pak.

Передайте объект как есть конструктору класса; Никаких условностей или проверок

interface iPerson {
   name: string;
   age: number;
}

class Person {
   constructor(private person: iPerson) { }

   toString(): string {
      return this.person.name + ' is ' + this.person.age;
   }  
}


// runs this as // 
const object1 = { name: 'Watson1', age: 64 };
const object2 = { name: 'Watson2' };            // age is missing

const person1 = new Person(object1);
const person2 = new Person(object2 as iPerson); // now matches constructor

console.log(person1.toString())  // Watson1 is 64
console.log(person2.toString())  // Watson2 is undefined

Вы можете использовать этот пакет npm. https://www.npmjs.com/package/class-converter

Его легко использовать, например:

class UserModel {
  @property('i')
  id: number;

  @property('n')
  name: string;
}

const userRaw = {
  i: 1234,
  n: 'name',
};

// use toClass to convert plain object to class
const userModel = toClass(userRaw, UserModel);
// you will get a class, just like below one
// const userModel = {
//   id: 1234,
//   name: 'name',
// }

Вы можете сделать что-то вроде этого:

      // Map method
function mapJSONtoObj<Type>(json: any, arg: Type): Type {
  Object.keys(json).forEach((elem:string)=>{
    let iterable:any = arg;
    let markerType = 0

    // we need to work around the fact that null and array are both objects
    markerType |= typeof(json[elem]) == 'object'?1:0
    markerType |= Array.isArray(json[elem])?2:0
    markerType |= json[elem]==null?4:0

    switch(markerType)
    {
      case 1:
      mapJSONtoObj(json[elem], iterable[elem])
      break;
      default:
      iterable[elem] = json[elem];
    }
  })
  return arg;
}

// Using example

class Internal {
  internVar: number = 0
}
class InternalArr {
  internArrVar: number = 0
}
class Root {
  rNum: number = 0
  internArrOfObj: InternalArr[] = []
  internObj: Internal = new Internal()
} 

let json = {
  rNum:1,
  internArrOfObj: [{
    internArrVar: 2
  }],
  internObj: {
    internVar:3
  }
}

let at = new Root()
let to = mapJSONtoObj(json, at);

И, конечно же, вы можете переписать классы карт в интерфейсы и использовать их как:

      let at = new Object as Root // if Root is interface

// And, finally, in "case 1" we should handle the condition that the object is undefined

// your mapJSONtoObj function
...
 switch(markerType)
    {
      case 1:
      iterable[elem] = {} // <---- add this one
      mapJSONtoObj(json[elem], iterable[elem])
      break;
      default:
      iterable[elem] = json[elem];
    }
...

Одна вещь, которую мы сделали, потому что мы являемся магазином.NET, это создание этого инструмента ( https://github.com/Centeva/TypeScripter).

Он строит классы машинописи из наших библиотек. Все, что нам нужно сделать, это направить наш ответ json в класс в качестве параметра. Это работает отлично для нас.

Вы можете сделать это с помощью одного файла tapi.js! Это легкий автомаппер, который работает в обоих направлениях.

      npm i -D tapi.js

Тогда вы можете просто сделать

      let typedObject = new YourClass().fromJSON(jsonData)

или с обещаниями

      axios.get(...).as(YourClass).then(typedObject => { ... })

Вы можете прочитать об этом больше в документации.

Другие вопросы по тегам