Como analisar string JSON em Typescript

99

Existe uma maneira de analisar strings como JSON em Typescript.
Exemplo: Em JS, podemos usar JSON.parse(). Existe uma função semelhante em Typescript?

Eu tenho uma string de objeto JSON da seguinte maneira:

{"name": "Bob", "error": false}
ssd20072
fonte
1
Em sua página inicial, ele diz que "TypeScript é um superconjunto tipado de JavaScript que compila para JavaScript simples". A função JSON.parse () deve ser utilizável normalmente.
sigalor
1
Estou usando o editor de texto Atom e quando faço um JSON.parse, recebo o erro: O argumento do tipo '{}' não pode ser atribuído ao parâmetro do tipo 'string'
ssd20072
21
Esta é uma questão muito básica e pode parecer trivial para alguns, mas é uma questão válida mesmo assim, e um equivalente não pode ser encontrado no SO (eu não encontrei), então não há razão real para não manter a questão em execução e, na minha opinião, também não deveria ser rejeitado.
Nitzan Tomer
2
@SanketDeshpande Quando você usa, JSON.parseobtém um objeto como resultado e não um string(veja minha resposta para mais informações). Se você quiser transformar um objeto em uma string, você precisa usar JSON.stringify.
Nitzan Tomer
2
Na verdade, não é uma pergunta simples por 2 motivos. Em primeiro lugar, JSON.parse () não retorna o mesmo tipo de objeto - ele corresponderá a parte da interface, mas nada inteligente, como acessadores, não estará presente. Além disso, certamente queremos que o SO esteja onde as pessoas vão quando pesquisam coisas no Google?
espécie desconhecida

Respostas:

165

Texto tipográfico é (um superconjunto de) javascript, então você apenas usa JSON.parsecomo faria em javascript:

let obj = JSON.parse(jsonString);

Apenas que no texto digitado você pode ter um tipo para o objeto resultante:

interface MyObj {
    myString: string;
    myNumber: number;
}

let obj: MyObj = JSON.parse('{ "myString": "string", "myNumber": 4 }');
console.log(obj.myString);
console.log(obj.myNumber);

( código no playground )

Nitzan Tomer
fonte
9
como validar se a entrada é válida (verificação de tipo, um dos propósitos do texto digitado)? substituir a entrada '{ "myString": "string", "myNumber": 4 }'por '{ "myString": "string", "myNumberBAD": 4 }'não falhará e obj.myNumber retornará indefinido.
David Portabella,
2
@DavidPortabella Você não pode ter verificação de tipo no conteúdo de uma string. É um problema de tempo de execução e a verificação de tipo é para o tempo de compilação
Nitzan Tomer
2
Está bem. como posso validar se um obj de texto digitado satisfaz sua interface em tempo de execução? ou seja, esse myNumber não é indefinido neste exemplo. por exemplo, no Scala Play, você usaria Json.parse(text).validate[MyObj]. playframework.com/documentation/2.6.x/ScalaJson como você pode fazer o mesmo no texto datilografado (talvez haja uma biblioteca externa para fazer isso?)?
David Portabella
1
@DavidPortabella Não tem como fazer isso, não facilmente, porque em tempo de execução MyObjnão existe. Existem muitos outros tópicos no SO sobre este assunto, por exemplo: Verifique se um objeto implementa uma interface em tempo de execução com TypeScript
Nitzan Tomer
7
ok obrigado. a cada dia fico mais convencido sobre o uso de scalajs.
David Portabella
5

Tipo seguro JSON.parse

Você pode continuar a usar JSON.parse , já que TS é um superconjunto JS. Resta um problema: JSON.parsedevoluções any, o que prejudica a segurança de tipo. Aqui estão duas opções para tipos mais fortes:

1. Protetores de tipo definido pelo usuário ( playground )

Protetores de tipo personalizado são a solução mais simples e muitas vezes suficientes para validação de dados externos:

// For example, you expect to parse a given value with `MyType` shape
type MyType = { name: string; description: string; }

// Validate this value with a custom type guard
function isMyType(o: any): o is MyType {
  return "name" in o && "description" in o
}

UMA JSON.parse wrapper pode então pegar um protetor de tipo como entrada e retornar o valor digitado analisado:

const safeJsonParse = <T>(guard: (o: any) => o is T) => (text: string): ParseResult<T> => {
  const parsed = JSON.parse(text)
  return guard(parsed) ? { parsed, hasError: false } : { hasError: true }
}

type ParseResult<T> =
  | { parsed: T; hasError: false; error?: undefined }
  | { parsed?: undefined; hasError: true; error?: unknown }
Exemplo de uso:
const json = '{ "name": "Foo", "description": "Bar" }';
const result = safeJsonParse(isMyType)(json) // result: ParseResult<MyType>
if (result.hasError) {
  console.log("error :/")  // further error handling here
} else {
  console.log(result.parsed.description) // result.parsed now has type `MyType`
}

safeJsonParse pode ser estendido para falhar rapidamente ou JSON.parseerros de tentativa / captura .

2. Bibliotecas externas

Escrever funções de proteção de tipo manualmente torna-se complicado, se você precisar validar muitos valores diferentes. Existem bibliotecas para auxiliar nesta tarefa - exemplos (nenhuma lista abrangente):

Mais informações

ford04
fonte
4

Se você deseja que seu JSON tenha um tipo Typecript validado, você mesmo precisará fazer esse trabalho de validação. Isso não é novidade. Em Javascript simples, você precisa fazer o mesmo.

Validação

Gosto de expressar minha lógica de validação como um conjunto de "transformações". Eu defino a Descriptorcomo um mapa de transformações:

type Descriptor<T> = {
  [P in keyof T]: (v: any) => T[P];
};

Então, posso criar uma função que aplicará essas transformações a uma entrada arbitrária:

function pick<T>(v: any, d: Descriptor<T>): T {
  const ret: any = {};
  for (let key in d) {
    try {
      const val = d[key](v[key]);
      if (typeof val !== "undefined") {
        ret[key] = val;
      }
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      throw new Error(`could not pick ${key}: ${msg}`);
    }
  }
  return ret;
}

Agora, não estou apenas validando minha entrada JSON, mas estou construindo um tipo de escrita à medida que prossigo. Os tipos genéricos acima garantem que o resultado infere os tipos de suas "transformações".

No caso de a transformação gerar um erro (que é como você implementaria a validação), gostaria de envolvê-la com outro erro mostrando qual chave causou o erro.

Uso

Em seu exemplo, eu usaria isso da seguinte maneira:

const value = pick(JSON.parse('{"name": "Bob", "error": false}'), {
  name: String,
  error: Boolean,
});

Agora valueserão digitados, visto que Stringe Booleansão ambos "transformadores" no sentido de que recebem a entrada e retornam uma saída digitada.

Além disso, o valueserá realmente desse tipo. Em outras palavras, se namefosse realmente 123, ele será transformado para "123"para que você tenha uma string válida. Isso ocorre porque usamos Stringem tempo de execução, uma função integrada que aceita entrada arbitrária e retorna umstring .

Você pode ver isso funcionando aqui . Tente o seguinte para se convencer:

  • Passe o mouse sobre a const valuedefinição para ver se o pop-over mostra o tipo correto.
  • Tente mudar "Bob"para 123e execute novamente a amostra. No console, você verá que o nome foi convertido corretamente para a string "123".
chowey
fonte
você deu um exemplo, "se namefosse realmente 123, ele será transformado para "123". Isso parece estar incorreto. Meu não valuevoltará quando eu copiar e colar todo o seu código exatamente e fizer essa alteração.{name: 123..{name:"123"..
Joncom
Estranho, funciona para mim. Experimente aqui: typescriptlang.org/play/index.html (usando em 123vez de "Bob").
chowey
Não acho que você precise definir um Transformedtipo. Você pode apenas usar Object. type Descriptor<T extends Object> = { ... };
lovasoa
Obrigado @lovasoa, você está correto. O Transformedtipo é totalmente desnecessário. Eu atualizei a resposta de acordo.
chowey
1

Além disso, você pode usar bibliotecas que realizam validação de tipo de seu json, como Sparkson . Eles permitem que você defina uma classe TypeScript, para a qual você gostaria de analisar sua resposta, no seu caso poderia ser:

import { Field } from "sparkson";
class Response {
   constructor(
      @Field("name") public name: string,
      @Field("error") public error: boolean
   ) {}
}

A biblioteca irá validar se os campos obrigatórios estão presentes na carga JSON e se seus tipos estão corretos. Ele também pode fazer várias validações e conversões.

jfu
fonte
Você deve mencionar que é o principal contribuidor da biblioteca acima.
ford04 de
1

Há uma ótima biblioteca para isso ts-json-object

No seu caso, você precisaria executar o seguinte código:

import {JSONObject, required} from 'ts-json-object'

class Response extends JSONObject {
    @required
    name: string;

    @required
    error: boolean;
}

let resp = new Response({"name": "Bob", "error": false});

Esta biblioteca irá validar o json antes de analisar

Elisha Sterngold
fonte
0

JSON.parse está disponível em TypeScript, então você pode apenas usá-lo:

JSON.parse('{"name": "Bob", "error": false}') // Returns a value of type 'any'

No entanto, você frequentemente desejará analisar um objeto JSON enquanto verifica se ele corresponde a um determinado tipo, em vez de lidar com um valor do tipo any. Nesse caso, você pode definir uma função como a seguinte:

function parse_json<TargetType extends Object>(
  json: string,
  type_definitions: { [Key in keyof TargetType]: (raw_value: any) => TargetType[Key] }
): TargetType {
  const raw = JSON.parse(json); 
  const result: any = {};
  for (const key in type_definitions) result[key] = type_definitions[key](raw[key]);
  return result;
}

Essa função pega uma string JSON e um objeto contendo funções individuais que carregam cada campo do objeto que você está criando. Você pode usá-lo assim:

const value = parse_json(
  '{"name": "Bob", "error": false}',
  { name: String, error: Boolean, }
);
Lovasoa
fonte