Сериализация объектов машинописи?

Существуют ли какие-либо средства для сериализации / десериализации JSON объектов Typescript, чтобы они не теряли информацию о типах? просто JSON.parse(JSON.stringify) слишком много предостережений.

Или я должен использовать специальные решения?

9 ответов

Решение

Используйте интерфейсы, чтобы получить сильные типы:

// Creating 
var foo:any = {};
foo.x = 3;
foo.y='123';

var jsonString = JSON.stringify(foo);
alert(jsonString);


// Reading
interface Bar{
    x:number;
    y?:string; 
}

var baz:Bar = JSON.parse(jsonString);
alert(baz.y);

И используйте утверждение типа "<>", если вам нужно.

Я думаю, что лучший способ справиться с этим - использовать Object.assign (который, однако, требует ECMAScript 2015).

Учитывая класс

class Pet {
    name: string;
    age: number;
    constructor(name?: string, age?: number) {
        this.name = name;
        this.age = age;
    }
    getDescription(): string {
        return "My pet " + this.name + " is " + this.age + " years old.";
    }
    static fromJSON(d: Object): Pet {
        return Object.assign(new Pet(), d);
    }
}

Сериализация и десериализация, как это...

var p0 = new Pet("Fido", 5);
var s = JSON.stringify(p0);
var p1 = Pet.fromJSON(JSON.parse(s));
console.log(p1.getDescription());

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

class Type {
    kind: string;
    breed: string;
    constructor(kind?: string, breed?: string) {
        this.kind = kind;
        this.breed = breed;
    }
    static fromJSON(d: Object) {
        return Object.assign(new Type(), d);
    }
}
class Pet {
    name: string;
    age: number;
    type: Type;
    constructor(name?: string, age?: number) {
        this.name = name;
        this.age = age;
    }
    getDescription(): string {
        return "My pet " + this.name + " is " + this.age + " years old.";
    }
    getFullDescription(): string {
        return "My " + this.type.kind + ", a " + this.type.breed + ", is " + this.age + " years old.";
    }
    static fromJSON(d: Object): Pet {
        var o = Object.assign(new Pet(), d);
        o.type = Type.fromJSON(o['type']);
        return o;
    }
}

Сериализация и десериализация, как это...

var q0 = new Pet("Fido", 5);
q0.type = new Type("dog", "Pomeranian");
var t = JSON.stringify(q0);
var q1 = Pet.fromJSON(JSON.parse(t));
console.log(q1.getFullDescription());

Таким образом, в отличие от использования интерфейса, этот подход сохраняет методы.

Лучшим методом, который я нашел до сих пор, было использование "jackson-js", jackson-js - это проект, который позволяет вам описывать класс с помощью ts-декораторов, а затем сериализовать и требовать сохранения информации о типе. он поддерживает массивы, карты и т. д.

полное руководство: https://itnext.io/jackson-js-powerful-javascript-decorators-to-serialize-deserialize-objects-into-json-and-vice-df952454cf

простой пример:

      import { JsonProperty, JsonClassType, JsonAlias, ObjectMapper } from 'jackson-js';

class Book {
  @JsonProperty() @JsonClassType({type: () => [String]})
  name: string;
  @JsonProperty() @JsonClassType({type: () => [String]})
  @JsonAlias({values: ['bkcat', 'mybkcat']})
  category: string;
}

class Writer {
  @JsonProperty() @JsonClassType({type: () => [Number]})
  id: number;
  @JsonProperty() @JsonClassType({type: () => [String]})
  name: string;
  @JsonProperty() @JsonClassType({type: () => [Array, [Book]]})
  books: Book[] = [];
}

const objectMapper = new ObjectMapper();
// eslint-disable-next-line max-len
const jsonData = '{"id":1,"name":"John","books":[{"name":"Learning TypeScript","bkcat":"Web Development"},{"name":"Learning Spring","mybkcat":"Java"}]}';
const writer = objectMapper.parse<Writer>(jsonData, {mainCreator: () => [Writer]});
console.log(writer);
/*
Writer {
  books: [
    Book { name: 'Learning TypeScript', category: 'Web Development' },
    Book { name: 'Learning Spring', category: 'Java' }
  ],
  id: 1,
  name: 'John'
}
*/

Есть несколько других проектов, которые утверждают, что делают то же самое -

Однако jackson-js - единственный, который работал у меня, когда я использовал Ts Map.

Я бы также предложил использовать ts-jackson

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

Во-первых, вам нужно создать интерфейс вашей исходной сущности, который вы получаете от API как JSON:

       interface UserEntity {
  name: string,
  age: number,
  country_code: string 
};

Во-вторых, реализуйте свою модель с помощью конструктора, в котором вы можете настроить (преобразовать) некоторые имена полей:

         class User  { 
    constructor({ name, age, country_code: countryCode }: UserEntity) {
      Object.assign(this, { name, age, countryCode });
    }
  }

Последний экземпляр вашей модели пользователя создается с помощью объекта JS jsonUser"

           const jsonUser = {name: 'Ted', age: 2, country_code: 'US'};

    const userInstance = new User(jsonUser);
    
    console.log({ userInstance })

Вот ссылка на детскую площадку

Ответ AQuirky работает для меня. У вас могут возникнуть проблемы с методом Object.assign. Мне пришлось изменить мой tsconfig.json, чтобы включить:

"compilerOptions": {
    ...
    "lib": ["es2015"],
    ...
}

Для Typescript гораздо менее многословен, чем ts-jackson, поскольку вы пишете сопоставления только тогда, когда это полезно (когда вам нужен экземпляр класса, а не для базовых типов JS):

      npm install @badcafe/jsonizer

Простой случай:

      import { Reviver, Jsonizer } from '@badcafe/jsonizer';

@Reviver<Book>({
    //   bind the reviver to the class
    '.': Jsonizer.Self.assign(Book)
    //   '.' key is the Self builder, that will assign each field
})
class Book {
  name: string;
  category: string;
}

@Reviver<Writer>({
  '.': Jsonizer.Self.assign(Writer),
  birthDate: Date, //  I have added this field in the class
  books: { //  Typescript knows it's an array
    '*': Book
    //   '*' key is the "any item" matcher for arrays
  }
})
class Writer {
  id: number;
  name: string;
  birthDate: Date;
  books: Book[] = [];
}

// eslint-disable-next-line max-len
const jsonData = '{"id":1,"name":"John","birthDate":"1990-12-31","books":[{"name":"Learning TypeScript","bkcat":"Web Development"},{"name":"Learning Spring","mybkcat":"Java"}]}';

const writerReviver = Reviver.get(Writer); //   extract the reviver from the class
// just use standard Javascript parse function :
const writer = JSON.parse(jsonData, writerReviver);
//      it's an instance of Writer, and in the code, it's type is inferred to be a Writer (usually, it is TS any)

Ниже, с некоторыми вариантами:

      import { Reviver } from '@badcafe/jsonizer';

// if the JSON has not the same shape of the class,
// we may describe it (usually, it's a DTO) :
interface BookDTO {
    name: string
    category?: string
    bkcat?: string
    mybkcat?: string
}

@Reviver<Book, BookDTO>({
    //  this time, we have a custom builder
    '.': ({ name, category, bkcat, mybkcat}) => {
             const book = new Book();
             book.name = name;
             book.category = category ?? bkcat ?? mybkcat;
             return book;
         }
})
class Book {
  name: string;
  category: string;
}

Ниже у нас есть вариант класса Writer.

  1. он может быть определен в другой библиотеке и импортирован, но Jsonizer не навязчив, и мы можем привязать возрождение к импортированным классам.
  2. вместо этого у нас есть конструктор с аргументами полей

Во-первых, класс:

      export class Writer {
  constructor( //  we have to pass args to the constructor
    public id: number,
    public name: string,
    public birthDate: Date,
    public books: Book[] = []
  ) {}
}
      import { Reviver, Jsonizer } from '@badcafe/jsonizer';
import { Writer, Book } from '...';

//  it is no longer a decorator
Reviver<Writer>({
  '.': Jsonizer.Self.apply(Writer), //  'apply' instead of 'assign'
  birthDate: Date,
  books: {
    '*': Book
  }
})(Writer) //  bind the reviver to the class

// eslint-disable-next-line max-len
const jsonData = '{"id":1,"name":"John","books":[{"name":"Learning TypeScript","bkcat":"Web Development"},{"name":"Learning Spring","mybkcat":"Java"}]}';

const writerReviver = Reviver.get(Writer);
const writer = JSON.parse(jsonData, writerReviver);

Поскольку вы можете использовать помощников строителей, таких какJsonizer.Self.assign()иJsonizer.Self.apply()а также специальные конструкторы, как в примере выше, легко воссоздать другие структуры, такие какMap; чтобы сериализовать его, просто определите стандартtoJSON()функция для вашего класса.

@badcafe/jsonizerможет сделать гораздо больше, например:

  • если у вас есть несколько классов с одинаковым именем, например, писателя иCategoryкниг вы можете назначить им пространство имен (это очень полезно, если вы импортируете классы, созданные в других библиотеках, их имена могут конфликтовать!)
  • если вы заранее не знаете, какой ревивер применить, в документации есть дополнительные приемы, показывающие, как динамически выбрать нужный ревивер и даже как отправить ревивер с данными полезной нагрузки и как реанимировать ревивер

https://badcafe.github.io/jsonizer/https://badcafe.github.io/jsonizer/

Я думаю, что лучше использовать эту библиотеку. Это упрощает сериализацию / десериализацию объекта / json.

https://www.npmjs.com/package/@peerlancers/json-serialization

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

Это нарушает принцип RAII и может/будет сбивать с толку пользователей этого класса, которые могут попасть в ловушку создания неполного Pet (нигде не указано явно, что за вызовом конструктора без аргументов должен следовать вызов fromJSON() для заполнения объект).

Итак, основываясь на его ответе, вот один из способов, используя цепочку прототипов JavaScript , вернуть объект класса после сериализации/десериализации. Ключевой трюк заключается в том, чтобы просто переназначить правильный объект-прототип после сериализации и десериализации:

      class Foo {}
foo1 = new Foo();
foo2 = JSON.parse(JSON.stringify(p1))
foo2.__proto__ = Foo.prototype;

Таким образом, чтобы исправить пример AQuirky с помощью этого трюка, мы могли бы просто изменить егоfromJSONфункционировать, чтобы

      static fromJSON(d: Object): Pet {
    d.__proto__ = Pet.prototype;
    return p
}
Другие вопросы по тегам