flutter internationalization using dynamic string in json file - json

So far I was using dynamic strings as shown in the solution of this post:
Flutter internationalization - Dynamic strings
Here's an example:
AppLocalizations.of(context).userAge(18)
And on AppLocalizations.dart:
userAge(age) => Intl.message(
"My age is $age",
name: "userAge",
args: [age]);
// Return "My age is 18"
But then I read this article about flutter internationalization: https://medium.com/flutter-community/flutter-internationalization-the-easy-way-using-provider-and-json-c47caa4212b2
Which shows how to localize using json files as resource files for the strings. It looks way more convenient so I prefer to use this method, but don't know how to get strings from json file with dynamic values.
Any solution?

To get the string with the dynamic value from JSON file you can use
final age = 18 //user input.
final ageString = 'user_age'
.localisedString()
.replaceAll(new RegExp(r'\${age}'), age)
en.json
{
"user_age": "My age is ${age}",
"user_name_age": "My name is ${name} and age is ${age}"
}
string_extension.dart
extension Localisation on String {
String localisedString() {
return stringBy(this) ?? '';
}
}
Also, you could do something like,
String localisedString(Map<String, String> args) {
String str = localisedString();
args.forEach((key, value) {
str = str.replaceAll(new RegExp(r'\${'+key+'}'), value);
});
return str;
}
//usecase
final userName = 'Spider Man'
final age = '18'
final nameAgeString = 'user_name_age'.localisedString({'name': userName, 'age': age})
app_localisation.dart
Map<String, dynamic> _language;
String stringBy(String key) => _language[key] as String ?? 'null';
class AppLocalisationDelegate extends LocalizationsDelegate {
const AppLocalisationDelegate();
// override the following method if you want to specify the locale you are supporting.
final _supportedLocale = ['en'];
#override
bool isSupported(Locale locale) => _supportedLocale.contains(locale.languageCode);
#override
Future load(Locale locale) async {
String jsonString = await rootBundle
.loadString("assets/strings/${locale.languageCode}.json");
_language = jsonDecode(jsonString) as Map<String, dynamic>;
print(_language.toString());
return SynchronousFuture<AppLocalisationDelegate>(
AppLocalisationDelegate());
}
#override
bool shouldReload(AppLocalisationDelegate old) => false;
}

Create a folder say json in your assets directory. Put your language files in it.
assets
json
- en.json // for English
- ru.json // for Russian
Now in en.json, write your string, for example.
{
"myAge": "My age is"
}
Similarly, in ru.json,
{
"myAge": "Мой возраст"
}
Add this to the pubspec.yaml file (mind the spaces)
flutter:
uses-material-design: true
assets:
- assets/json/
Run flutter pub get
Initial work done. Let's move to the code side.
Copy this boilerplate code in your file:
Map<String, dynamic> language;
class AppLocalizations {
static AppLocalizations of(BuildContext context) {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
String getText(String key) => language[key];
String userAge(int age) => '${getText('myAge')} $age';
}
class AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
const AppLocalizationsDelegate();
#override
bool isSupported(Locale locale) => ['en', 'ru'].contains(locale.languageCode);
#override
Future<AppLocalizations> load(Locale locale) async {
final string = await rootBundle.loadString('assets/json/${locale.languageCode}.json');
language = json.decode(string);
return SynchronousFuture<AppLocalizations>(AppLocalizations());
}
#override
bool shouldReload(AppLocalizationsDelegate old) => false;
}
Set up few things in MaterialApp widget:
void main() {
runApp(
MaterialApp(
locale: Locale('ru'), // switch between "en" and "ru" to see effect
localizationsDelegates: [const AppLocalizationsDelegate()],
supportedLocales: [const Locale('en'), const Locale('ru')],
home: HomePage(),
),
);
}
Now, you can simply use above delegate:
class HomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
var age = AppLocalizations.of(context).userAge(18);
// prints "My age is 18" for 'en' and "Мой возраст 18" for 'ru' locale.
print(age);
return Scaffold();
}
}

I've tried various solutions to implement localization, and the best I've come across is the Flutter Intl plugin for VS Code or Android Studio/IntelliJ made by Localizely.com (not affiliated).
With it, basically you install the plugin using the marketplace/plugin library, then initialize for your project using the menu option. This creates a default english locale in lib/l10n/intl_en.arb (which sounds scary but is actually just JSON) and sets up all the scaffolding for the internationalization in lib/generated.
You also have to add the following to your dependencies.
flutter_localizations:
sdk: flutter
You can then add keys to this file and they'll be automatically available in your app, by importing generated/l10n.dart which contains a class called S.
To get flutter to use it, wherever it is that you initialize your MaterialApp, make sure to pass S.delegate into MaterialApp's localizationsDelegates parameter (most likely as part of an array with GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, and possibly GlobalCupertinoLocalizations.delegate.) You also have to add S.delegate.supportedLocales to MaterialApp's supportedLocales.
To add more locales, use the option in the menu (in intellij at least) or simply create more intl_.arb files, and the plugin will automatically recognize this and set up the relevant code.
Say you have an intl_en file with the following:
{ "name": "Name" }
You'd then use S.of(context).name to use the string in your code.
All this is more eloquently explained on localizely's website.
Now, to use keys in these .arb files, you simply have to wrap it in {...}. So for example:
{ "choose1OfNumOptions": "Choose 1 of {numoptions} options" }
would lead to a usage of S.of(context).choose1OfNumOptions(numOptions);. I don't know that the plugin supports the full ARB specification but it does support at least the basics.
Also, I'm not using Localizely but it seems like it'd be a pretty useful way to manage the translations and the plugin integrates automatically, although I think it's also pretty horrendously overpriced - at least for my app, which happens to have a ton of text. I actually just have a google sheet where I store all my translations, and when it's time to update it I download it as a .tsv and wrote a simple little parser to write out to the .arb files.

flutter_localizations:
app_en.arb:
{
"contactDetailsPopupEmailCopiedMessage": "Copied {email} to clipboard",
"#contactDetailsPopupEmailCopiedMessage": {
"description": "Message being displayed in a snackbar upon long-clicking email in contact details popup",
"placeholders": {
"email": {
"type": "String",
"example": "example#gmail.com"
}
}
}
}
app_localizations.dart:
abstract class AppLocalizations {
/// Message being displayed in a snackbar upon long-clicking email in contact details popup
///
/// In en, this message translates to:
/// **'Copied {email} to clipboard'**
String contactDetailsPopupEmailCopiedMessage(String email);
}
usage:
l10n.contactDetailsPopupEmailCopiedMessage("example#gmail.com")
For more details take a look here.

Related

Can we use reflection to instantiate custom TypeScript model from a JSON string?

Is it possible to clone a JSON-generated object or string into a Typescript class which I created? We are building a model of our API using Typescript classes. There’s a base class which they all extend which has common/helper methods. When we do JSON.parse(response) to auto-generate objects it creates simple objects and not our custom objects.
Is there a way we can convert those JSON-generated objects into our custom objects, so long as the field names match up? And, to make things more robust, can this but done where our custom objects’ fields are other custom objects and/or arrays of them?
Here is our code, with comments of what we’d like to achieve.
base-model.ts
export class BaseModelObject {
uuid: string; // All of our objects in our model and JSON have this required field populated
matchUUIDs<T extends BaseModelObject>( obj: T): boolean {
return obj.uuid == this.uuid;
}
}
child-model.ts
import { BaseModelObject } from 'base-model';
export class Child extends BaseModelObject {
}
parent-model.ts
import { BaseModelObject } from 'base-model';
import { Child } from 'child-model';
export class Parent extends BaseModelObject {
children: Child[];
}
JSON payload
{
'uuid': '0632a35c-e7dd-40a8-b5f4-f571a8359c1a',
'children': [
{
'uuid': 'd738c408-4ae9-430d-a64d-ba3f085175fc'
},
{
'uuid': '44d56a0d-ad2d-4e85-b5d1-da4371fc0e5f'
}
]
}
In our components and directives and such, we hope to use the helper function in BaseModelObject:
Component code
let parent: Parent = JSON.parse(response);
console.log(parent.uuid); // Works! 0632a35c-e7dd-40a8-b5f4-f571a8359c1a
// Want this to print ‘true’, but instead we get TypeError: parebt.matchUUID is not a function
console.log(parent.matchUUID(‘0632a35c-e7dd-40a8-b5f4-f571a8359c1a’));
// Want this to print ‘true’, but instead we get TypeError: parent.children[0].matchUUID is not a function
console.log(parent.children[0].matchUUID(‘d738c408-4ae9-430d-a64d-ba3f085175fc’));
The problem is that JSON.parse() is not creating our classes, it’s creating simple objects with key/value pairs. So we’re thinking of “cloning” the JSON-generated object into an instance of our class, like this:
base-model.ts
export class BaseModelObject {
[key: string]: any;
matchUUIDs<T extends BaseModelObject>( obj: T): boolean {
return obj['uuid'] == this['uuid'];
}
cloneFields(obj: any) {
for (let prop in obj) {
this[prop] = obj[prop];
}
}
}
Component code
let parent: Parent = new Parent(); // Creates instance of our class
parent.cloneFields(JSON.parse(response)); // Copy JSON fields to our object
console.log(parent.matchUUID('0632a35c-e7dd-40a8-b5f4-f571a8359c1a')); // prints 'true'
console.log(parent.children[0].matchUUID('d738c408-4ae9-430d-a64d-ba3f085175fc')); // Still throws TypeError: parent.children[0].matchUUID is not a function
The problem now rests in the fact that the cloning of the Parent object did not recursively clone the JSON-generated Child objects into instances of our custom Child class.
Since our Parent object is typed at compile-time and it knows that the data type of the children array is Child[] (our custom class), is there a way to use reflection to instantiate the right class?
Our logic would need to say:
Create an instance of our custom class
Tell our instance to clone the fields from the JSON-generated object
Iterate over the fields in the JSON-generated object
For each field name from the JSON-generated object, find the "type definition" in our custom class
If the type definition is not a primitive or native Typescript type, then instantiate a new instance of that "type" and then clone it's fields.
(and it would need to recursively traverse the whole JSON object structure to match up all other custom classes/objects we add to our model).
So something like:
cloneFields(obj: any) {
for (let prop in obj) {
let A: any = ...find the data type of prop...
if(...A is a primitive type ...) {
this[prop] = obj[prop];
} else {
// Yes, I know this code won't compile.
// Just trying to illustrate how to instantiate
let B: <T extends BaseModelUtil> = ...instantiate an instance of A...
B.cloneFields(prop);
A[prop] = B;
}
}
}
Is it possible to reflect a data type from a class variable definition and then instantiate it at runtime?
Or if I'm going down an ugly rabbit hole to which you know a different solution, I'd love to hear it. We simply want to build our custom objects from a JSON payload without needing to hand-code the same patterns over and over since we expect our model to grow into dozens of objects and hundreds of fields.
Thanks in advance!
Michael
There are several ways to do that, but some requires more work and maintenance than others.
1. Simple, a lot of work
Make your cloneFields abstract and implement it in each class.
export abstract class BaseModelObject {
uuid: string;
matchUUIDs<T extends BaseModelObject>( obj: T): boolean {
return obj.uuid == this.uuid;
}
abstract cloneFields(obj: any);
}
class Parent extends BaseModelObject {
children: Child[];
cloneFields(obj: any) {
this.children = obj.children?.map(child => {
const c = new Children();
c.cloneFields(child);
return c;
});
}
}
2. Simple, hacky way
If there is no polymorphism like:
class Parent extends BaseModelObject {
children: Child[] = [ new Child(), new ChildOfChild(), new SomeOtherChild() ]
}
Property names mapped to types.
const Map = {
children: Child,
parent: Parent,
default: BaseModelObject
}
export class BaseModelObject {
uuid: string;
matchUUIDs<T extends BaseModelObject>( obj: T): boolean {
return obj.uuid == this.uuid;
}
cloneFields(obj: any) {
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
this[prop] = obj[prop]?.map(child => { // You have to check it is an array or not,..
const c = new (Map[prop])();
c.cloneFields(child);
return c;
});
}
}
}
}
You can serialize hints into that JSON. Eg. property type with source/target type name and use it to resolve right types.
3. Reflection
Try tst-reflect. It is pretty advanced Reflection system for TypeScript (using custom typescript transformer plugin).
I'm not going to write example, it would be too complex and it depends on your needs.
You can use tst-reflect to list type's properties and get their types. So you'll be able to validace parsed data too.
Just some showcase from its README:
import { getType } from "tst-reflect";
function printTypeProperties<TType>()
{
const type = getType<TType>(); // <<== get type of generic TType ;)
console.log(type.getProperties().map(prop => prop.name + ": " + prop.type.name).join("\n"));
}
interface SomeType {
foo: string;
bar: number;
baz: Date;
}
printTypeProperties<SomeType>();
// or direct
getType<SomeType>().getProperties();
EDIT:
I created a package ng-custom-transformers that simplifies this a lot. Follow its README.
DEMO
EDIT old:
Usage with Angular
Angular has no direct support of custom transformers/plugins. There is a feature request in the Angular Github repo.
But there is a workaround.
You have to add ngx-build-plus. Run ng add ngx-build-plus.
That package defines "plugins".
Plugins allow you to provide some custom code that modifies your webpack configuration.
So you can create plugin and extend Angular's webpack configuration. But here comes the sun problem. There is no public way to add the transformer. There were AngularCompilerPlugin in webpack configuration (#ngtools/webpack) which had private _transformers property. It was possible to add a transformer into that array property. But AngularCompilerPlugin has been replaced by AngularWebpackPlugin which has no such property. But is is possible to override method of AngularWebpackPlugin and add a transformer there. Getting an instance of the AngularWebpackPlugin is possible thanks to ngx-build-plus's plugins.
Code of the plugin
const {AngularWebpackPlugin} = require("#ngtools/webpack");
const tstReflectTransform = require("tst-reflect-transformer").default;
module.exports.default = {
pre() {},
post() {},
config(cfg) {
// Find the AngularWebpackPlugin in the webpack configuration; angular > 12
const angularWebpackPlugin = cfg.plugins.find((plugin) => plugin instanceof AngularWebpackPlugin);
if (!angularWebpackPlugin) {
console.error("Could not inject the typescript transformer: AngularWebpackPlugin not found");
return;
}
addTransformerToAngularWebpackPlugin(angularWebpackPlugin, transformer);
return cfg;
},
};
function transformer(builderProgram) {
return tstReflectTransform(builderProgram.getProgram());
}
function addTransformerToAngularWebpackPlugin(plugin, transformer) {
const originalCreateFileEmitter = plugin.createFileEmitter; // private method
plugin.createFileEmitter = function (programBuilder, transformers, getExtraDependencies, onAfterEmit, ...rest) {
if (!transformers) {
transformers = {};
}
if (!transformers.before) {
transformers = {before: []};
}
transformers.before = [transformer(programBuilder), ...transformers.before];
return originalCreateFileEmitter.apply(plugin, [programBuilder, transformers, getExtraDependencies, onAfterEmit, ...rest]);
};
}
Then it is required to execute ng commands (such as serve or build) with --plugin path/to/the/plugin.js.
I've made working StackBlitz demo.
Resources I've used while preparing the Angular demo:
https://indepth.dev/posts/1045/having-fun-with-angular-and-typescript-transformers
https://medium.com/#morrys/custom-typescript-transformers-with-angular-for-angular-11-12-and-13-40cbdc9cca7b

How can I convert a list of custom objects within a parent object to (and from) JSON?

I have an app where I am trying to convert custom objects that the user inputs to and from JSON so I can store them in shared preferences.
Each object is a 'Resolution' and has multiple property types (Dates, Colors etc...) which I have no problem converting. So far so good...
My problem is that one of its properties is a List of type 'Commitment'. A 'Commitment' is ANOTHER custom object which also has properties of many different types (dates, doubles, strings etc..).
I've discovered I cannot simply convert the whole List property into a string and back, I need to run a specific conversion for all of the individual properties within each commitment within the list.
So I've basically got a nested conversion requirement...
The code for my 'RESOLUTION' is as follows
class Resolution {
final String id;
String title;
late DateTime? startDate;
late DateTime? endDate;
Color color;
double progress;
List<Commitment> commitments;
bool isComplete;
Resolution(
{required this.id,
required this.title,
this.startDate,
required this.endDate,
this.color = Colors.orange,
this.progress = 0,
this.commitments = const [],
this.isComplete = false});
Map toMap() {
return {
'id': id,
'title': title,
'startDate': startDate!.toIso8601String(),
'endDate': endDate!.toIso8601String(),
'color': color.value.toString(),
'progress': progress,
'commitments': convertCommitmentsToJSON(commitments),
'isComplete': isComplete,
};
}
Resolution.fromMap(Map map)
: id = map['id'],
title = map['title'],
startDate = DateTime.tryParse(map['startDate']),
endDate = DateTime.tryParse(map['endDate']),
color = Color(int.parse(map['color'])),
progress = map['progress'],
commitments = List<Commitment>.from(map['commitments']),
isComplete = map['isComplete'];
convertCommitmentsToJSON(commitments) {
for (Commitment commitment in commitments) {
commitment.toMap();
}
}
This works perfectly EXCEPT when it comes to that List Commitment property... As you can see I have written the following method
convertCommitmentsToJSON(commitments) {
for (Commitment commitment in commitments) {
commitment.toMap();
}
}
Which I THINK is successfully converting the list to JSON by looping through the list contents and applying a conversion method I have created within the COMMITMENT class.
Assuming I have that half of the problem solved (which I may not so feel free to chip in), my issue is converting that JSON back into a list of Commitment objects...
I tried the following...
convertCommitmentsFromJSON(List<Commitment> list) {
for (Commitment commitment in list) {
commitment.fromMap(jsonDecode(commitment));
}
}
...but I get the message 'The static method 'fromMap' can't be accessed through an instance'
The content of my (simplified) COMMITMENT class is as follows if it helps
class Commitment {
String description;
DateTime? endDate;
Commitment({required this.description, this.endDate});
Map<String, dynamic> toMap() => {
'description': description,
'endDate': endDate?.toIso8601String(),
};
static Commitment fromMap(Map<String, dynamic> json) => Commitment(
description: json['description'],
endDate: DateTime.tryParse(json['endDate']),
);
}
Can anyone help? I'm relatively new to coding so perhaps i'm completely down the wrong path
REVISED CONVERSION CODE
Map toJson() {
return {
'id': id,
'title': title,
'startDate': startDate!.toIso8601String(),
'endDate': endDate!.toIso8601String(),
'color': color.value.toString(),
'progress': progress,
'commitments': convertCommitmentsToJSON(commitments),
'isComplete': isComplete,
};
}
Resolution.fromJson(Map map)
: id = map['id'],
title = map['title'],
startDate = DateTime.tryParse(map['startDate']),
endDate = DateTime.tryParse(map['endDate']),
color = Color(int.parse(map['color'])),
progress = map['progress'],
commitments = convertCommitmentsFromJSON(List<Commitment>.from(map['commitments'])),
isComplete = map['isComplete'];
convertCommitmentsToJSON(commitments) {
for (Commitment commitment in commitments) {
commitment.toJson();
}
}
convertCommitmentsFromJSON(List<Commitment> list) {
for (Commitment commitment in list) {
Commitment.fromJson;
}
}
To access a static function, you need to use the class name, not the instance
Commitment.fromMap(jsonDecode(commitment));
FYI: the convention is fromJson and toJson
Also: better to make fromMap a factory constructor ... again, convention.
factory Commitment.fromJson(Map<String, dynamic> json) => Commitment(
description: json['description'],
endDate: DateTime.tryParse(json['endDate']),
);
Managed to resolve in the end with Kevin's help. syntax aside a key thing i was missing is that i wasn't actually returning a list in the original version of this function. I was simply looping through and doing nothing with the output.
Kevin, thanks for the assist. Got there in the end! ;)
static List<Commitment> convertCommitmentsFromJSON(List<dynamic> list) {
List<Commitment> commitmentsList = [];
if (list.isNotEmpty) {
for (var item in list) {
Commitment commitment = Commitment.fromJson(item);
commitmentsList.add(commitment);
}
}
return commitmentsList;
}

create object structure of type from JSON.parse object [duplicate]

I read a JSON object from a remote REST server. This JSON object has all the properties of a typescript class (by design). How do I cast that received JSON object to a type var?
I don't want to populate a typescript var (ie have a constructor that takes this JSON object). It's large and copying everything across sub-object by sub-object & property by property would take a lot of time.
Update: You can however cast it to a typescript interface!
You can't simple cast a plain-old-JavaScript result from an Ajax request into a prototypical JavaScript/TypeScript class instance. There are a number of techniques for doing it, and generally involve copying data. Unless you create an instance of the class, it won't have any methods or properties. It will remain a simple JavaScript object.
While if you only were dealing with data, you could just do a cast to an interface (as it's purely a compile time structure), this would require that you use a TypeScript class which uses the data instance and performs operations with that data.
Some examples of copying the data:
Copying AJAX JSON object into existing Object
Parse JSON String into a Particular Object Prototype in JavaScript
In essence, you'd just :
var d = new MyRichObject();
d.copyInto(jsonResult);
I had the same issue and I have found a library that does the job : https://github.com/pleerock/class-transformer.
It works like this :
let jsonObject = response.json() as Object;
let fooInstance = plainToClass(Models.Foo, jsonObject);
return fooInstance;
It supports nested children but you have to decorate your class's member.
In TypeScript you can do a type assertion using an interface and generics like so:
var json = Utilities.JSONLoader.loadFromFile("../docs/location_map.json");
var locations: Array<ILocationMap> = JSON.parse(json).location;
Where ILocationMap describes the shape of your data. The advantage of this method is that your JSON could contain more properties but the shape satisfies the conditions of the interface.
However, this does NOT add class instance methods.
If you are using ES6, try this:
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()
})
But this method will not work on nested objects, sadly.
I found a very interesting article on generic casting of JSON to a Typescript Class:
http://cloudmark.github.io/Json-Mapping/
You end up with following code:
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
There is nothing yet to automatically check if the JSON object you received from the server has the expected (read is conform to the) typescript's interface properties. But you can use User-Defined Type Guards
Considering the following interface and a silly json object (it could have been any type):
interface MyInterface {
key: string;
}
const json: object = { "key": "value" }
Three possible ways:
A. Type Assertion or simple static cast placed after the variable
const myObject: MyInterface = json as MyInterface;
B. Simple static cast, before the variable and between diamonds
const myObject: MyInterface = <MyInterface>json;
C. Advanced dynamic cast, you check yourself the structure of the object
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}'.`);
}
You can play with this example here
Note that the difficulty here is to write the isMyInterface function. I hope TS will add a decorator sooner or later to export complex typing to the runtime and let the runtime check the object's structure when needed. For now, you could either use a json schema validator which purpose is approximately the same OR this runtime type check function generator
TLDR: One liner
// This assumes your constructor method will assign properties from the arg.
.map((instanceData: MyClass) => new MyClass(instanceData));
The Detailed Answer
I would not recommend the Object.assign approach, as it can inappropriately litter your class instance with irrelevant properties (as well as defined closures) that were not declared within the class itself.
In the class you are trying to deserialize into, I would ensure any properties you want deserialized are defined (null, empty array, etc). By defining your properties with initial values you expose their visibility when trying to iterate class members to assign values to (see deserialize method below).
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];
}
}
}
}
In the example above, I simply created a deserialize method. In a real world example, I would have it centralized in a reusable base class or service method.
Here is how to utilize this in something like an http resp...
this.http.get(ENDPOINT_URL)
.map(res => res.json())
.map((resp: Person) => new Person(resp) ) );
If tslint/ide complains about argument type being incompatible, just cast the argument into the same type using angular brackets <YourClassName>, example:
const person = new Person(<Person> { name: 'John', age: 35, id: 1 });
If you have class members that are of a specific type (aka: instance of another class), then you can have them casted into typed instances through getter/setter methods.
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));
}
}
The above example will deserialize both acct and the list of tasks into their respective class instances.
Assuming the json has the same properties as your typescript class, you don't have to copy your Json properties to your typescript object. You will just have to construct your Typescript object passing the json data in the constructor.
In your ajax callback, you receive a company:
onReceiveCompany( jsonCompany : any )
{
let newCompany = new Company( jsonCompany );
// call the methods on your newCompany object ...
}
In in order to to make that work:
1) Add a constructor in your Typescript class that takes the json data as parameter. In that constructor you extend your json object with jQuery, like this: $.extend( this, jsonData). $.extend allows keeping the javascript prototypes while adding the json object's properties.
2) Note you will have to do the same for linked objects. In the case of Employees in the example, you also create a constructor taking the portion of the json data for employees. You call $.map to translate json employees to typescript Employee objects.
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);
}
}
This is the best solution I found when dealing with Typescript classes and json objects.
In my case it works. I used functions
Object.assign (target, sources ...).
First, the creation of the correct object, then copies the data from json object to the target.Example :
let u:User = new User();
Object.assign(u , jsonUsers);
And a more advanced example of use. An example using the array.
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;
}
}
While it is not casting per se; I have found https://github.com/JohnWhiteTB/TypedJSON to be a useful alternative.
#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"
Personally I find it appalling that typescript does not allow an endpoint definition to specify
the type of the object being received. As it appears that this is indeed the case,
I would do what I have done with other languages, and that is that I would separate the JSON object from the class definition,
and have the class definition use the JSON object as its only data member.
I despise boilerplate code, so for me it is usually a matter of getting to the desired result with the least amount of code while preserving type.
Consider the following JSON object structure definitions - these would be what you would receive at an endpoint, they are structure definitions only, no methods.
interface IAddress {
street: string;
city: string;
state: string;
zip: string;
}
interface IPerson {
name: string;
address: IAddress;
}
If we think of the above in object oriented terms, the above interfaces are not classes because they only define a data structure.
A class in OO terms defines data and the code that operates on it.
So we now define a class that specifies data and the code that operates on it...
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;
}
}
And now we can simply pass in any object conforming to the IPerson structure and be on our way...
Person person = new Person({
name: "persons name",
address: {
street: "A street address",
city: "a city",
state: "a state",
zip: "A zipcode"
}
});
In the same fashion we can now process the object received at your endpoint with something along the lines of...
Person person = new Person(req.body); // As in an object received via a POST call
person.distanceFrom({ street: "Some street address", etc.});
This is much more performant and uses half the memory of copying the data, while significantly reducing the amount of boilerplate code you must write for each entity type.
It simply relies on the type safety provided by TypeScript.
Use a class extended from an interface.
Then:
Object.assign(
new ToWhat(),
what
)
And best:
Object.assign(
new ToWhat(),
<IDataInterface>what
)
ToWhat becomes a controller of DataInterface
If you need to cast your json object to a typescript class and have its instance methods available in the resulting object you need to use Object.setPrototypeOf, like I did in the code snippet bellow:
Object.setPrototypeOf(jsonObject, YourTypescriptClass.prototype)
Use 'as' declaration:
const data = JSON.parse(response.data) as MyClass;
An old question with mostly correct, but not very efficient answers. This what I propose:
Create a base class that contains init() method and static cast methods (for a single object and an array). The static methods could be anywhere; the version with the base class and init() allows easy extensions afterwards.
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() { }
}
Similar mechanics (with assign()) have been mentioned in #Adam111p post. Just another (more complete) way to do it. #Timothy Perez is critical of assign(), but imho it is fully appropriate here.
Implement a derived (the real) class:
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);
}
}
}
Now we can cast an object retrieved from service:
const area = ContentItem.castAs<SubjectArea>(docFromREST, SubjectArea);
All hierarchy of SubjectArea objects will have correct class.
A use case/example; create an Angular service (abstract base class again):
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;
}
}
The usage becomes very simple; create an Area service:
#Injectable()
export class SubjectAreaService extends BaseService<SubjectArea> {
path = 'area';
subClass = SubjectArea;
constructor(protected http: Http) { super(); }
}
get() method of the service will return a Promise of an array already cast as SubjectArea objects (whole hierarchy)
Now say, we have another class:
export class OtherItem extends ContentItem {...}
Creating a service that retrieves data and casts to the correct class is as simple as:
#Injectable()
export class OtherItemService extends BaseService<OtherItem> {
path = 'other';
subClass = OtherItem;
constructor(protected http: Http) { super(); }
}
You can create an interface of your type (SomeType) and cast the object in that.
const typedObject: SomeType = <SomeType> responseObject;
FOR JAVA LOVERS
Make POJO class
export default class TransactionDTO{
constructor() {
}
}
create empty object by class
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/
you can use this site to generate a proxy for you. it generates a class and can parse and validate your input JSON object.
I used this library here: https://github.com/pleerock/class-transformer
<script lang="ts">
import { plainToClass } from 'class-transformer';
</script>
Implementation:
private async getClassTypeValue() {
const value = await plainToClass(ProductNewsItem, JSON.parse(response.data));
}
Sometimes you will have to parse the JSON values for plainToClass to understand that it is a JSON formatted data
In the lates TS you can do like this:
const isMyInterface = (val: any): val is MyInterface => {
if (!val) { return false; }
if (!val.myProp) { return false; }
return true;
};
And than user like this:
if (isMyInterface(data)) {
// now data will be type of MyInterface
}
I ran into a similar need.
I wanted something that will give me easy transformation from/to JSON
that is coming from a REST api call to/from specific class definition.
The solutions that I've found were insufficient or meant to rewrite my
classes' code and adding annotations or similars.
I wanted something like GSON is used in Java to serialize/deserialize classes to/from JSON objects.
Combined with a later need, that the converter will function in JS as well, I ended writing my own package.
It has though, a little bit of overhead. But when started it is very convenient in adding and editing.
You initialize the module with :
conversion schema - allowing to map between fields and determine
how the conversion will be done
Classes map array
Conversion functions map - for special conversions.
Then in your code, you use the initialized module like :
const convertedNewClassesArray : MyClass[] = this.converter.convert<MyClass>(jsonObjArray, 'MyClass');
const convertedNewClass : MyClass = this.converter.convertOneObject<MyClass>(jsonObj, 'MyClass');
or , to JSON :
const jsonObject = this.converter.convertToJson(myClassInstance);
Use this link to the npm package and also a detailed explanation to how to work with the module: json-class-converter
Also wrapped it for
Angular use in :
angular-json-class-converter
Pass the object as is to the class constructor; No conventions or checks
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
You can use this npm package. https://www.npmjs.com/package/class-converter
It is easy to use, for example:
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',
// }
You can with a single tapi.js!
It's a lightweight automapper that works in both ways.
npm i -D tapi.js
Then you can simply do
let typedObject = new YourClass().fromJSON(jsonData)
or with promises
axios.get(...).as(YourClass).then(typedObject => { ... })
You can read more about it on the docs.
There are several ways to do it, lets examine a some options:
class Person {
id: number | undefined;
firstName: string | undefined;
//? mark for note not required attribute.
lastName?: string;
}
// Option 1: Fill any attribute and it would be accepted.
const person1= { firstName: 'Cassio' } as Person ;
console.log(person1);
// Option 2. All attributes must assign data.
const person2: Person = { id: 1, firstName: 'Cassio', lastName:'Seffrin' };
console.log(person2);
// Option 3. Use partial interface if all attribute not required.
const person3: Partial<Person> = { firstName: 'Cassio' };
console.log(person3);
// Option 4. As lastName is optional it will work
const person4: Person = { id:2, firstName: 'Cassio' };
console.log(person4);
// Option 5. Fill any attribute and it would be accepted.
const person5 = <Person> {firstName: 'Cassio'};
console.log(person5 );
Result:
[LOG]: {
"firstName": "Cassio"
}
[LOG]: {
"id": 1,
"firstName": "Cassio",
"lastName": "Seffrin"
}
[LOG]: {
"firstName": "Cassio"
}
[LOG]: {
"id": 2,
"firstName": "Cassio"
}
[LOG]: {
"firstName": "Cassio"
}
It will also work if you have an interface instead a Typescript class.
interface PersonInterface {
id: number;
firstName: string;
lastName?: string;
}
Play this code
I think that json2typescript is a good alternative
https://www.npmjs.com/package/json2typescript
You can convert json to Class model with a simple model class with annotations
Used in project
You can cast json to property like this
class Jobs {
constructor(JSONdata) {
this.HEAT = JSONdata.HEAT;
this.HEAT_EAF = JSONdata.HEAT_EAF;
}
}
var job = new Jobs({HEAT:'123',HEAT_EAF:'456'});
This is a simple and a really good option
let person = "{"name":"Sam","Age":"30"}";
const jsonParse: ((key: string, value: any) => any) | undefined = undefined;
let objectConverted = JSON.parse(textValue, jsonParse);
And then you'll have
objectConverted.name

How do I cast a JSON Object to a TypeScript class?

I read a JSON object from a remote REST server. This JSON object has all the properties of a typescript class (by design). How do I cast that received JSON object to a type var?
I don't want to populate a typescript var (ie have a constructor that takes this JSON object). It's large and copying everything across sub-object by sub-object & property by property would take a lot of time.
Update: You can however cast it to a typescript interface!
You can't simple cast a plain-old-JavaScript result from an Ajax request into a prototypical JavaScript/TypeScript class instance. There are a number of techniques for doing it, and generally involve copying data. Unless you create an instance of the class, it won't have any methods or properties. It will remain a simple JavaScript object.
While if you only were dealing with data, you could just do a cast to an interface (as it's purely a compile time structure), this would require that you use a TypeScript class which uses the data instance and performs operations with that data.
Some examples of copying the data:
Copying AJAX JSON object into existing Object
Parse JSON String into a Particular Object Prototype in JavaScript
In essence, you'd just :
var d = new MyRichObject();
d.copyInto(jsonResult);
I had the same issue and I have found a library that does the job : https://github.com/pleerock/class-transformer.
It works like this :
let jsonObject = response.json() as Object;
let fooInstance = plainToClass(Models.Foo, jsonObject);
return fooInstance;
It supports nested children but you have to decorate your class's member.
In TypeScript you can do a type assertion using an interface and generics like so:
var json = Utilities.JSONLoader.loadFromFile("../docs/location_map.json");
var locations: Array<ILocationMap> = JSON.parse(json).location;
Where ILocationMap describes the shape of your data. The advantage of this method is that your JSON could contain more properties but the shape satisfies the conditions of the interface.
However, this does NOT add class instance methods.
If you are using ES6, try this:
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()
})
But this method will not work on nested objects, sadly.
I found a very interesting article on generic casting of JSON to a Typescript Class:
http://cloudmark.github.io/Json-Mapping/
You end up with following code:
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
There is nothing yet to automatically check if the JSON object you received from the server has the expected (read is conform to the) typescript's interface properties. But you can use User-Defined Type Guards
Considering the following interface and a silly json object (it could have been any type):
interface MyInterface {
key: string;
}
const json: object = { "key": "value" }
Three possible ways:
A. Type Assertion or simple static cast placed after the variable
const myObject: MyInterface = json as MyInterface;
B. Simple static cast, before the variable and between diamonds
const myObject: MyInterface = <MyInterface>json;
C. Advanced dynamic cast, you check yourself the structure of the object
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}'.`);
}
You can play with this example here
Note that the difficulty here is to write the isMyInterface function. I hope TS will add a decorator sooner or later to export complex typing to the runtime and let the runtime check the object's structure when needed. For now, you could either use a json schema validator which purpose is approximately the same OR this runtime type check function generator
TLDR: One liner
// This assumes your constructor method will assign properties from the arg.
.map((instanceData: MyClass) => new MyClass(instanceData));
The Detailed Answer
I would not recommend the Object.assign approach, as it can inappropriately litter your class instance with irrelevant properties (as well as defined closures) that were not declared within the class itself.
In the class you are trying to deserialize into, I would ensure any properties you want deserialized are defined (null, empty array, etc). By defining your properties with initial values you expose their visibility when trying to iterate class members to assign values to (see deserialize method below).
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];
}
}
}
}
In the example above, I simply created a deserialize method. In a real world example, I would have it centralized in a reusable base class or service method.
Here is how to utilize this in something like an http resp...
this.http.get(ENDPOINT_URL)
.map(res => res.json())
.map((resp: Person) => new Person(resp) ) );
If tslint/ide complains about argument type being incompatible, just cast the argument into the same type using angular brackets <YourClassName>, example:
const person = new Person(<Person> { name: 'John', age: 35, id: 1 });
If you have class members that are of a specific type (aka: instance of another class), then you can have them casted into typed instances through getter/setter methods.
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));
}
}
The above example will deserialize both acct and the list of tasks into their respective class instances.
Assuming the json has the same properties as your typescript class, you don't have to copy your Json properties to your typescript object. You will just have to construct your Typescript object passing the json data in the constructor.
In your ajax callback, you receive a company:
onReceiveCompany( jsonCompany : any )
{
let newCompany = new Company( jsonCompany );
// call the methods on your newCompany object ...
}
In in order to to make that work:
1) Add a constructor in your Typescript class that takes the json data as parameter. In that constructor you extend your json object with jQuery, like this: $.extend( this, jsonData). $.extend allows keeping the javascript prototypes while adding the json object's properties.
2) Note you will have to do the same for linked objects. In the case of Employees in the example, you also create a constructor taking the portion of the json data for employees. You call $.map to translate json employees to typescript Employee objects.
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);
}
}
This is the best solution I found when dealing with Typescript classes and json objects.
In my case it works. I used functions
Object.assign (target, sources ...).
First, the creation of the correct object, then copies the data from json object to the target.Example :
let u:User = new User();
Object.assign(u , jsonUsers);
And a more advanced example of use. An example using the array.
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;
}
}
While it is not casting per se; I have found https://github.com/JohnWhiteTB/TypedJSON to be a useful alternative.
#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"
Personally I find it appalling that typescript does not allow an endpoint definition to specify
the type of the object being received. As it appears that this is indeed the case,
I would do what I have done with other languages, and that is that I would separate the JSON object from the class definition,
and have the class definition use the JSON object as its only data member.
I despise boilerplate code, so for me it is usually a matter of getting to the desired result with the least amount of code while preserving type.
Consider the following JSON object structure definitions - these would be what you would receive at an endpoint, they are structure definitions only, no methods.
interface IAddress {
street: string;
city: string;
state: string;
zip: string;
}
interface IPerson {
name: string;
address: IAddress;
}
If we think of the above in object oriented terms, the above interfaces are not classes because they only define a data structure.
A class in OO terms defines data and the code that operates on it.
So we now define a class that specifies data and the code that operates on it...
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;
}
}
And now we can simply pass in any object conforming to the IPerson structure and be on our way...
Person person = new Person({
name: "persons name",
address: {
street: "A street address",
city: "a city",
state: "a state",
zip: "A zipcode"
}
});
In the same fashion we can now process the object received at your endpoint with something along the lines of...
Person person = new Person(req.body); // As in an object received via a POST call
person.distanceFrom({ street: "Some street address", etc.});
This is much more performant and uses half the memory of copying the data, while significantly reducing the amount of boilerplate code you must write for each entity type.
It simply relies on the type safety provided by TypeScript.
Use a class extended from an interface.
Then:
Object.assign(
new ToWhat(),
what
)
And best:
Object.assign(
new ToWhat(),
<IDataInterface>what
)
ToWhat becomes a controller of DataInterface
If you need to cast your json object to a typescript class and have its instance methods available in the resulting object you need to use Object.setPrototypeOf, like I did in the code snippet bellow:
Object.setPrototypeOf(jsonObject, YourTypescriptClass.prototype)
Use 'as' declaration:
const data = JSON.parse(response.data) as MyClass;
An old question with mostly correct, but not very efficient answers. This what I propose:
Create a base class that contains init() method and static cast methods (for a single object and an array). The static methods could be anywhere; the version with the base class and init() allows easy extensions afterwards.
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() { }
}
Similar mechanics (with assign()) have been mentioned in #Adam111p post. Just another (more complete) way to do it. #Timothy Perez is critical of assign(), but imho it is fully appropriate here.
Implement a derived (the real) class:
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);
}
}
}
Now we can cast an object retrieved from service:
const area = ContentItem.castAs<SubjectArea>(docFromREST, SubjectArea);
All hierarchy of SubjectArea objects will have correct class.
A use case/example; create an Angular service (abstract base class again):
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;
}
}
The usage becomes very simple; create an Area service:
#Injectable()
export class SubjectAreaService extends BaseService<SubjectArea> {
path = 'area';
subClass = SubjectArea;
constructor(protected http: Http) { super(); }
}
get() method of the service will return a Promise of an array already cast as SubjectArea objects (whole hierarchy)
Now say, we have another class:
export class OtherItem extends ContentItem {...}
Creating a service that retrieves data and casts to the correct class is as simple as:
#Injectable()
export class OtherItemService extends BaseService<OtherItem> {
path = 'other';
subClass = OtherItem;
constructor(protected http: Http) { super(); }
}
You can create an interface of your type (SomeType) and cast the object in that.
const typedObject: SomeType = <SomeType> responseObject;
FOR JAVA LOVERS
Make POJO class
export default class TransactionDTO{
constructor() {
}
}
create empty object by class
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/
you can use this site to generate a proxy for you. it generates a class and can parse and validate your input JSON object.
I used this library here: https://github.com/pleerock/class-transformer
<script lang="ts">
import { plainToClass } from 'class-transformer';
</script>
Implementation:
private async getClassTypeValue() {
const value = await plainToClass(ProductNewsItem, JSON.parse(response.data));
}
Sometimes you will have to parse the JSON values for plainToClass to understand that it is a JSON formatted data
In the lates TS you can do like this:
const isMyInterface = (val: any): val is MyInterface => {
if (!val) { return false; }
if (!val.myProp) { return false; }
return true;
};
And than user like this:
if (isMyInterface(data)) {
// now data will be type of MyInterface
}
I ran into a similar need.
I wanted something that will give me easy transformation from/to JSON
that is coming from a REST api call to/from specific class definition.
The solutions that I've found were insufficient or meant to rewrite my
classes' code and adding annotations or similars.
I wanted something like GSON is used in Java to serialize/deserialize classes to/from JSON objects.
Combined with a later need, that the converter will function in JS as well, I ended writing my own package.
It has though, a little bit of overhead. But when started it is very convenient in adding and editing.
You initialize the module with :
conversion schema - allowing to map between fields and determine
how the conversion will be done
Classes map array
Conversion functions map - for special conversions.
Then in your code, you use the initialized module like :
const convertedNewClassesArray : MyClass[] = this.converter.convert<MyClass>(jsonObjArray, 'MyClass');
const convertedNewClass : MyClass = this.converter.convertOneObject<MyClass>(jsonObj, 'MyClass');
or , to JSON :
const jsonObject = this.converter.convertToJson(myClassInstance);
Use this link to the npm package and also a detailed explanation to how to work with the module: json-class-converter
Also wrapped it for
Angular use in :
angular-json-class-converter
Pass the object as is to the class constructor; No conventions or checks
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
You can use this npm package. https://www.npmjs.com/package/class-converter
It is easy to use, for example:
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',
// }
You can with a single tapi.js!
It's a lightweight automapper that works in both ways.
npm i -D tapi.js
Then you can simply do
let typedObject = new YourClass().fromJSON(jsonData)
or with promises
axios.get(...).as(YourClass).then(typedObject => { ... })
You can read more about it on the docs.
There are several ways to do it, lets examine a some options:
class Person {
id: number | undefined;
firstName: string | undefined;
//? mark for note not required attribute.
lastName?: string;
}
// Option 1: Fill any attribute and it would be accepted.
const person1= { firstName: 'Cassio' } as Person ;
console.log(person1);
// Option 2. All attributes must assign data.
const person2: Person = { id: 1, firstName: 'Cassio', lastName:'Seffrin' };
console.log(person2);
// Option 3. Use partial interface if all attribute not required.
const person3: Partial<Person> = { firstName: 'Cassio' };
console.log(person3);
// Option 4. As lastName is optional it will work
const person4: Person = { id:2, firstName: 'Cassio' };
console.log(person4);
// Option 5. Fill any attribute and it would be accepted.
const person5 = <Person> {firstName: 'Cassio'};
console.log(person5 );
Result:
[LOG]: {
"firstName": "Cassio"
}
[LOG]: {
"id": 1,
"firstName": "Cassio",
"lastName": "Seffrin"
}
[LOG]: {
"firstName": "Cassio"
}
[LOG]: {
"id": 2,
"firstName": "Cassio"
}
[LOG]: {
"firstName": "Cassio"
}
It will also work if you have an interface instead a Typescript class.
interface PersonInterface {
id: number;
firstName: string;
lastName?: string;
}
Play this code
I think that json2typescript is a good alternative
https://www.npmjs.com/package/json2typescript
You can convert json to Class model with a simple model class with annotations
Used in project
You can cast json to property like this
class Jobs {
constructor(JSONdata) {
this.HEAT = JSONdata.HEAT;
this.HEAT_EAF = JSONdata.HEAT_EAF;
}
}
var job = new Jobs({HEAT:'123',HEAT_EAF:'456'});
This is a simple and a really good option
let person = "{"name":"Sam","Age":"30"}";
const jsonParse: ((key: string, value: any) => any) | undefined = undefined;
let objectConverted = JSON.parse(textValue, jsonParse);
And then you'll have
objectConverted.name

Breezejs Naming Convention withtout metadata

I've been reading answers and the Breeze documentation, digging in the code, but I just can't make this work.
I've a WebService with NancyFx that it's used by many webapps, and I decided to go for the convention of the camelCase in json.
'[{"id":"0", "slug":"my-slug","name":"Slug"}]'
And now I'm looking into breeze and I want to use oData query, I'm using Linq2Rest, so my first attempt module is like this..
private dynamic GetFoos(dynamic p)
{
//Converting the request params to NameValueCollection to be used by
//extensionmethod Filter of Linq2Rest
var nvc = this.BuildNameValueCollection();
var institutions = Repository.Get();
return Response.AsJson(institutions.Filter(nvc));
}
And this works like a charm when I do something like
http://foo.com/foos$filter=Slug%20eq%20my-slug
Ok, now let's play with Breeze....So I've my metadata by hand and I've something like this
function addFooModel() {
addType({
name: 'Foo',
dataProperties: {
id : { type : ID },
name : { nullOk: false },
slug : { nullOk: false },
}
});
};
And I query my service like this
var query = breeze.EntityQuery.from('foos')
.using(manager)
.where('slug', '==', slug)
.toType('Foo');
So this is where everything breaks, first of all since my property in .NET of my class Foo is declared like this...
public class Foo
{
public int Id {get; set;}
public string Name {get; set;}
public string Slug {get; set;}
}
So the url that is constructed by Breeze is http://foo.com/foos$filter=slug%20eq%20my-slug and since my property is in PascalCase Linq2Rest says noupe, there's no slug property.
If I declare the breeze.NamingConvention.camelCase.setAsDefault(); the url is correct but the data is not bound because it expects the json in PascalCase but the json returned is in camelcase
I've tried to create my namingConvention, setting nameOnServer on the dataProperty, create a jsonResultAdapter and I can not make it work....
I know I just can do this
var query = breeze.EntityQuery.from('foos')
.using(manager)
.where('Slug', '==', slug);
And this works but the data.results is not bound to the Foo model....
So is there a way to do this from breeze like:
Create the url in PascalCase and then resolve the json in CamelCase.
I did this workaround with jsonResultAdapter but I don't like it
var jsonResultsAdapter = new breeze.JsonResultsAdapter({
name: "fooAdapter",
extractResults: function(json) {
return json.results;
},
visitNode: function(node, mappingContext, nodeContext) {
for (var key in node){
node[key.substr(0, 1).toUpperCase() + key.substr(1)] = node[key];
}
return {
entityType: mappingContext.query.resultType
};
}
});
Suggestions....
Thanks!
EDIT.
Right now I'm using the camelCase configuration that comes with breeze breeze.NamingConvention.camelCase.setAsDefault(); the properties of my model is in camelCase and the jsonResultsAdapter I'm using is this one...
var jsonResultsAdapter = new breeze.JsonResultsAdapter({
name: "fooAdapter",
extractResults: function(json) {
return json.results;
},
visitNode: function(node, mappingContext, nodeContext) {
for (var key in node){
node[key.substr(0, 1).toUpperCase() + key.substr(1)] = node[key];
}
return {
entityType: mappingContext.query.resultType
};
}
});
I'm adding to the node the same properties but in PascalCase. If the server serves this json '[{"id":"0", "slug":"my-slug","name":"Slug"}]' after the adapter the node transforms in '[{"id":"0", "slug":"my-slug","name":"Slug", "Id":"0", "Slug":"my-slug","Name":"Slug"}]'and then I don't know where yet, Breeze maps the properties in PascalCase to camelCase because of the namingConvention that I'm using.
The query is this one
var query = breeze.EntityQuery.from('foos')
.using(manager)
.where('slug', '==', slug)
.toType('Foo');